# Promessa de estabilidade da CLI

> Aplica-se a partir da **v0.42.3**. Escrita para o `0.x` — cada quebra listada
> abaixo tinha de esperar por um major. **A v1.0.0 é esse major**: fechou a
> única quebra pendente (os atalhos de topo), e esta lista passa a valer como a
> promessa de semver real de resto em diante — um breaking change deixa de
> caber num `1.x`.

Um motor sem contrato não se automatiza. Quem escreve um `Makefile`, um passo de
CI ou um script de deploy precisa de saber o que pode partir num upgrade — e a
resposta «é 0.x, tudo pode partir» é verdadeira e inútil: garante que ninguém
depende de nada, o que é o mesmo que ninguém adoptar.

Isto listava o que se compromete e o que não se compromete dentro do `0.x`,
que é o que falta a maior parte dos projectos nessa fase. Desde a v1.0.0 é
mais do que isso: é o contrato de semver do projecto.

## Estável — não quebra sem um major

**Os verbos de ciclo de vida de container**, com os nomes e a semântica que
Docker e Podman lhes dão:

```
container run   ps   stop   start   restart   kill   rm   exec   logs
                wait   inspect   port   rename   pause   unpause
image     pull  ls    remove    build (delonix build)
```

Concretamente, garante-se:

* **O nome do comando e a ordem dos argumentos posicionais.**
* **As flags curtas e longas listadas acima e os seus significados** — em
  `run`: `-d`, `-p`, `-v`, `-e`, `--name`, `--rm`, `--net`, `--restart`,
  `--memory`, `--cpus`, `--entrypoint`, `-w`, `-u`, `--add-host`, `--wait`,
  `--health-*`; em `exec`: `-i`, `-t`, `-e`, `-w`, `-u`. (Esta lista andou
  achatada, sem dizer a qual verbo cada flag pertence — `-i`/`-t` nunca
  existiram em `run`, só em `exec`; um teste em `main.rs` verifica agora as
  duas listas contra a árvore `clap` real, em vez de confiar na leitura.)
* **Os códigos de saída** — ver a secção «Códigos de saída» abaixo.
* **A saída JSON de `inspect`** — campos podem ser ACRESCENTADOS, nunca removidos
  nem com o tipo mudado.

> **Quebra de contrato na v1.0.0.** Até à v0.69.0 os atalhos de topo (`ps`,
> `run`, `exec`, `logs`, `rm`, `images`) estavam aqui, como reescrita de argv
> para `container <verbo>`/`image list`. Saíram — corte limpo, sem alias: a
> grafia antiga falha com `unrecognized subcommand`, nunca em silêncio, a
> mesma regra que a reorganização da v0.30.0 já seguia. Os grupos continuam:
> `delonix container ps`, `delonix container run`, `delonix container exec`,
> `delonix container logs`, `delonix container rm`, `delonix image list`
> (ver a nota da v2.0.0 abaixo — essa grafia mudou outra vez).
> **Os códigos de saída não mudam nesta versão** — ver a secção abaixo.

> **Quebra de contrato na v2.0.0.** O B2 da reestruturação da CLI (v0.67.0)
> tinha renomeado `image ls`/`image rm` para `image list`/`image remove` — a
> única excepção de nomenclatura numa CLI onde as outras 15 folhas do tipo
> "listar" usam `ls` (`network ls`, `volume ls`, `vm ls`, `pod ls`, `secret
> ls`, `stack ls`, …), seguindo o padrão Docker/Podman que este mesmo
> documento já cita para os verbos de container. Revertido: `image list`
> volta a `image ls` (`image remove` fica). Corte limpo, sem alias — a
> grafia `list` falha com `unrecognized subcommand`, nunca em silêncio.

## Códigos de saída

> **Alteração de contrato na v0.49.0.** Até à v0.48.0 **toda** a falha do motor
> era `1` — «não existe» e «rebentou» eram o mesmo número. Continua a valer que
> `0` é sucesso e não-zero é falha, portanto um `if delonix …; then` não muda de
> comportamento; o que muda é para quem testa `[ $? -eq 1 ]` **à espera de uma
> falha específica**. Ver a nota de migração no fim desta secção.

| código | significado |
|---|---|
| `0` | sucesso |
| `1` | falha sem classe própria (o default de sempre) |
| `2` | uso inválido (o do `clap`) — e, em `stack plan --detailed-exitcode`, «há alterações» |
| `3` | o recurso existe mas **não está a correr** |
| `4` | **não existe** esse recurso |
| `5` | **conflito** — o nome já está tomado |
| `69` | **capacidade que este host não tem** — uma ferramenta por instalar, um backend indisponível |
| `74` | **o sistema de ficheiros disse que não** — disco cheio, caminho impossível |
| `77` | **permissão negada** — o remédio é uma permissão, e depois repetir |
| `124` | **o prazo esgotou-se** — `stack wait --timeout`, e o que vier a ter prazo |

`3` e `4` não são números inventados: são os códigos de estado do LSB que o
`systemctl` ainda fala (`3` = o programa não está a correr, `4` = não há tal
unidade). `5` não tem convenção por trás — é o número livre seguinte, abaixo da
gama que a shell usa (`126`/`127`, e `128+N` para sinais).

**`74` e `77` saíram do balde do `1`, e não de nenhuma classe publicada.** Até
à v0.66.1 uma falha de I/O respondia `1`, que a linha de cima descreve como
«falha sem classe própria» — dar-lhe classe é o que esta tabela existe para
fazer, e um script com `*) exit 1` continua a cair no mesmo ramo. O `77` é
recortado do `74` pelo `kind` do erro, porque é assim que ele chega: medido
contra um state root sem bit de escrita, `volumes create` e `secret create`
vêm ambos como permissão negada. É a falha mais accionável que existe — corrige
a permissão e repete — e respondia o mesmo número que um disco cheio.

**`69` e `124` também não.** `69` é o `EX_UNAVAILABLE` do `sysexits.h`; `124` é
o que o `timeout(1)` devolve quando o prazo passa, e está portanto já nos dedos
de quem embrulha um comando num. Entraram porque tinham **produtores reais mal
classificados**, não para completar uma tabela: um `stack wait` que esgotava o
tempo respondia `1` — o mesmo número de um apply rebentado, no comando cuja
função inteira é ser lido por CI — e um `wg`/`virt-customize`/`ngrok` em falta
respondia `1` também, indistinguível de um erro de escrita numa flag. As duas
chamadas seguintes de um reconciliador são opostas: esperar mais, ou parar e
instalar alguma coisa.

```bash
delonix stack wait -f delonix-manifest.yaml --timeout 120
case $? in
  0)   ;;                     # tudo de pé
  124) exit 0 ;;              # ainda a subir — o pipeline seguinte volta a tentar
  69)  echo "falta uma ferramenta neste nó" >&2; exit 1 ;;
  *)   exit 1 ;;              # qualquer outra coisa: pára
esac
```

### O número do dicionário: `DX-CDNN`

O código de saída e o `DX_*` dizem a **classe**. O número `DX-CDNN` diz **qual falha**
(ADR-0043): o milhar é a classe, a centena o domínio, os dois últimos a falha, e `00` a
própria classe. A CLI imprime-o na linha de erro — `error[DX-4501] no such VM: dev` — e
`delonix explain DX-4501` diz o que significa e o que fazer (`--json` para scripts). O
dicionário completo está em `delonix explain codes` e na página
[Dicionário de códigos](codigos.html), gerada da mesma tabela.

**Um número nunca muda de significado nem é reutilizado**: pode acrescentar-se, e uma falha
que deixe de existir fica com o número retirado. A mensagem ao lado pode ser reescrita e é
traduzida; o número é o contrato. Enquanto um sítio do motor ainda não tem entrada própria,
responde com a entrada genérica da sua classe (`DX-4000`, `DX-1000`, …).

### A identidade textual: `DX_*`

O número serve quem lê `$?`. Quem lê **texto** — um cliente HTTP, um consumidor
de `-o json`, um pipeline de logs — tem o código `DX_*`, que é a mesma
classificação noutra grafia:

| `DX_*` | código | quando |
|---|---|---|
| `DX_NOT_FOUND` | `4` | não existe esse recurso |
| `DX_NOT_RUNNING` | `3` | existe, mas não está a correr |
| `DX_CONFLICT` | `5` | o nome já está tomado |
| `DX_UNAVAILABLE` | `69` | capacidade que este host não tem |
| `DX_TIMEOUT` | `124` | o prazo esgotou-se |
| `DX_INVALID_ARGUMENT` · `DX_REGISTRY` · `DX_SYSCALL_FAILED` · `DX_INVALID_STATE` · `DX_IO` | `1` | falhas sem número próprio |

**Estes nomes são contrato**: um código pode ser ACRESCENTADO; um existente
nunca muda de grafia nem de significado.

A relação é **assimétrica de propósito**. Um `DX_*` mapeia sempre para UM
número — se mapeasse para dois, o `$?` e o texto contradiziam-se para a mesma
falha. Mas o `1` carrega VÁRIOS códigos, porque é o balde «sem classe própria» e
o texto pode dar-se ao luxo de ser mais fino: cada NÚMERO é uma promessa que tem
de valer o resto do `0.x`, enquanto `DX_REGISTRY` ao lado de
`DX_INVALID_ARGUMENT` não custa nada. Há teste a exigir as duas metades desta
regra, incluindo que o balde continue a ser um balde.

Hoje o código sai na **API de gestão** (`delonix serve api`), como campo
acrescentado ao corpo de erro — `{"error": "...", "code": "DX_NOT_FOUND"}`. O
campo `error` não foi tocado: a regra do ADR-0005 (acrescentar sim, remover ou
mudar de tipo não) vale aqui como vale no `-o json`.

**O que continua sem código próprio, e é honesto dizê-lo:** *permissão negada* e
*falha temporária* foram considerados e ficaram de fora — não há hoje uma
variante de erro que os produza (as falhas de permissão chegam embrulhadas no
`errno` de uma syscall, e o retry que existe acontece dentro do motor e nunca
chega a quem chama). Publicar um número que nada constrói é um número que nunca
pode voltar.

O que isto resolve é concreto: um reconciliador — o `Makefile`, o passo de CI, o
ciclo em bash que conduz esta CLI — não conseguia separar «cria, porque falta»
de «pára, porque falhou» sem ler a MENSAGEM de erro. E a mensagem é traduzida
(`--l18n=pt`), portanto um script que faz `grep 'no such'` funciona na máquina
onde foi escrito e deixa de classificar num nó com outra locale.

```bash
delonix container inspect web >/dev/null 2>&1
case $? in
  0) ;;                       # está lá
  4) delonix container run -d --name web nginx ;;   # falta — cria
  3) delonix container start web ;;                 # existe, parado — arranca
  *) exit 1 ;;                # qualquer outra coisa: pára, não adivinhes
esac
```

**O código de um `run`/`exec` continua a ser o do WORKLOAD, não o do motor.**
`run` em primeiro plano devolve o código do processo do container, `exec` o do
comando, e `healthcheck` sai `1` quando não saudável — as três promessas de
sempre, inalteradas. A consequência a reter: um container que saia `4` sai `4`,
logo **o `$?` de um `run`/`exec` nunca se lê como uma das classes acima** — esse
número foi escolhido pelo workload, não pelo motor.

**O que fica de fora**, e é honesto dizê-lo: uma pré-condição do host por
satisfazer (uma sessão sem delegação de cgroup, por exemplo) não tem código
próprio — o motor avisa e continua, portanto não há falha para classificar. E
os grupos ainda não estáveis (ver a lista mais abaixo) podem responder `1` onde
um comando estável já responde `4`: o `workload describe` de um nome que não
existe é o caso conhecido.

### Migrar

* `if delonix …; then` / `|| exit 1` / `set -e` — **nada muda**.
* `[ $? -eq 1 ]` a testar «este comando falhou» — passa a ser `[ $? -ne 0 ]`.
* `[ $? -eq 1 ]` a testar «não existe» — passa a ser `[ $? -eq 4 ]`, que é o
  teste que antes não havia maneira de escrever.

## Estável em conteúdo, não em formato

**As tabelas de `ls`/`ps`.** As colunas podem mudar de largura, de ordem ou
ganhar irmãs — são feitas para humanos e medem-se pelo conteúdo real. Um script
que faça `awk '{print $3}'` sobre elas parte, e isso não conta como quebra de
contrato.

**Para automação há `-o json`**, e é ele que é estável: um array JSON por
recurso, campos podem ser ACRESCENTADOS mas não removidos nem com o tipo mudado
(ADR-0005). Verificado a funcionar (2026-09-25, v4.4.0) em `container ps`,
`image ls`, `volume ls`, `network ls`, `vm ls`, `secret ls`, `workload ls` e no
verbo genérico `get <plural>` (`get pods`, `get volumes`, …), que é o caminho de
listagem dos Kinds sem `ls` próprio — o `pod ls` já não existe, e o grupo
`storage` fundiu-se no `volume`. Também `inspect` e `-q`/`--quiet`. **Excepção
medida:** `backup ls` ainda não aceita `-o json`.

> Uma versão anterior deste documento dizia que `-o json` estava «por fazer e é a
> lacuna reconhecida aqui». **Estava errado** — existe desde a ADR-0005. É
> exactamente o erro que o `paridade-docker-podman.md` abre por corrigir: inferir
> ausência a partir de um sintoma, sem ir ver.

## O schema dos manifestos — estável, e é o que mais importa

> Esta secção **substitui** a linha que dizia que o schema «é aditivo na prática,
> mas não é uma promessa». Era o compromisso ao contrário: a CLI estava mais
> protegida que o formato declarativo, quando é o formato que as pessoas põem em
> git e revêem em PR. Um `delonix-manifest.yaml` só se versiona se se souber que
> não parte sozinho.

Para os Kinds com spec tipado — **`Container`, `Pod`, `Volume`, `Network`** —
garante-se, dentro do `0.x`:

* **Um campo nunca é removido, nem muda de tipo, nem muda de significado.**
* **Um campo novo é sempre opcional** e tem um default que preserva o
  comportamento anterior. Um manifesto escrito hoje continua a fazer o mesmo
  amanhã.
* **Um nome antigo que seja renomeado mantém-se aceite como alias** (é o que já
  acontece com `restart`→`restartPolicy`, `options`→`mountOptions`,
  `wg_ip`→`wgIp`).
* **`apiVersion: delonix.io/v1` só muda com um `v2`**, e um `v2` não sai sem o
  `v1` continuar a ser aceite. Desde a v0.64.0 cada Kind tem também o grupo do
  seu domínio (`compute.delonix.io/v1alpha1`, `networking.…`, …) — o
  `delonix api-resources` diz qual é o de cada um. As duas grafias são aceites;
  a de grupo é a canónica, e o Kind só aceita **o grupo dele** ou o legado.

A verdade não é este texto, é o schema: **`delonix manifest schema`** emite-o a
partir do próprio código (ADR-0007), e o mesmo ficheiro está publicado em
[`schema/v1/delonix.json`](schema/v1/delonix.json). Um teste do repositório
falha se o publicado deixar de ser o gerado, precisamente para esta página não
voltar a poder mentir.

O que o schema RECUSA, e vale saber porque é o que aparece sublinhado no editor:
um campo desconhecido (um typo), **um `kind` que o motor não conhece** — um typo
no nome, ou um Kind removido — e **um `apiVersion` que não é o grupo daquele
Kind**. Até à v0.65.x o `kind` era uma string livre e qualquer nome passava, por
isso um `kind: Contaner` validava limpo e um `Egress` já removido também; e o
`apiVersion` era fixo no legado, por isso a grafia de grupo — a canónica — era
sublinhada como se fosse erro. Um validador que discorda do motor é pior do que
nenhum, porque o visto verde é o que as pessoas seguem.

Aponta o editor e escreve manifestos com completação e validação:

```yaml
# yaml-language-server: $schema=https://angolardevops.github.io/delonix-runtime/schema/v1/delonix.json
apiVersion: compute.delonix.io/v1alpha1
kind: Pod
metadata: { name: web }
spec:
  containers:
    - name: web
      image: nginx:1.27
```

Há também uma extensão de VS Code que traz isto já ligado, mais um template por
Kind: [`angolardevops/delonix-vscode`](https://github.com/angolardevops/delonix-vscode).
Aponta para o MESMO ficheiro publicado acima — buscado ao vivo, não embutido —
por isso o editor e o `stack apply` não podem discordar.

Para saber o que mudou entre duas versões, não há uma página escrita à mão —
haveria a segunda fonte de verdade que a ADR-0007 aboliu. Há um comando:

```bash
scripts/schema-diff.sh v0.46.0          # dessa tag até à árvore actual
scripts/schema-diff.sh v0.46.0 v0.47.0  # entre duas tags
```

Compara campo a campo (nome e tipo), não o JSON cru, e sai **1** quando há
diferenças — serve directamente como gate de CI. Um campo removido ou com o
tipo mudado é assinalado como **quebra de contrato**, que é o que esta secção
promete não acontecer.

**Todos os Kinds têm hoje schema gerado.** Esta secção dizia que `Vm`,
`Cluster`, `ShareVolume`, `Image`, `Secret`, `Ingress`, `FirewallPolicy`,
`HTTPRoute`, `Tunnel`, `Workload` e `Stack` não o tinham — deixou de ser
verdade: `delonix explain <Kind>` responde para cada Kind que
`delonix api-resources` lista (verificado a 2026-09-25, v4.4.0), com os nomes
canónicos (`VirtualMachine`, `KubernetesCluster`, `NetworkPolicy`, `Gateway`; os
nomes antigos `Vm`/`Cluster`/`FirewallPolicy`/`Tunnel` continuam a resolver como
alias). `ShareVolume` já não existe: é um `kind: Volume` com bloco `share:`. Se
um Kind futuro chegar sem schema, o `delonix explain` recusa-o a dizer que Kinds
o têm, em vez de o omitir.

> **Três Kinds deixaram de existir** nesta série, fundidos no que já faziam:
> `Egress` → `FirewallPolicy` com `direction: egress`; `Dependency` →
> `FirewallPolicy` (açúcar, reduzido no load); `Storage` → `kind: Volume` com um
> bloco `nfs:`/`cifs:`/`webdav:`. Os nomes antigos continuam a carregar, com
> aviso de depreciação — a regra do «corte limpo» aplica-se a comandos, e um
> manifesto em git merece um degrau em vez de um erro.

## O ficheiro de providers do nó — `config.delonix.io/v1`

O `providers.yaml` (ADR-0054) diz que providers o nó tem, como o motor chega a
cada um e qual serve um pedido que não nomeia nenhum. Desde o catálogo 1.1.0
(ADR-0059 F1) aceita também um appliance OPNsense (`type: opnsense`) e o bloco
`networkDefaults`, que diz que provider responde a cada papel de rede
(`segment`, `gateway`; `nat`, `ipam` e `dns` são recusados até existir quem os
sirva). O `defaultProvider` continua a ser só o de computação. Procura-se por
esta ordem, e **o primeiro ficheiro que existe ganha, sem fusão**:
`DELONIX_PROVIDERS_CONFIG`, `$XDG_CONFIG_HOME/delonix/providers.yaml` (ou
`~/.config/…`), `/etc/delonix/providers.yaml`.

É um formato publicado, com a mesma promessa que o schema dos manifestos dentro
do `0.x`: **uma chave nunca é removida nem muda de significado, uma chave nova é
sempre opcional, e `apiVersion: config.delonix.io/v1` só muda com um `v2`** que
não sai sem o `v1` continuar a ser lido.

A verdade é o schema, gerado dos tipos que o motor lê:
**`delonix provider config schema`**, publicado em
[`schema/v1/providers.json`](schema/v1/providers.json), com o mesmo teste a
falhar se o publicado deixar de ser o gerado. O schema recusa uma chave
desconhecida, outra versão, um tipo de provider que não existe e um segredo
escrito no próprio ficheiro (`tokenSecret`, `password`, `secret` — só por
referência: `tokenSecretFile`, `passwordFile`, `secretFile`, `secretRef`).

O schema diz se o ficheiro está **bem escrito**; o
**`delonix provider config validate`** diz se ele **serve**: um
`defaultProvider` ou um `networkDefaults` sem entrada no ficheiro, um ficheiro
de token ou de segredo legível por outros utilizadores, uma CA que não existe. Nenhum dos dois contacta um nó.

```yaml
# yaml-language-server: $schema=https://angolardevops.github.io/delonix-runtime/schema/v1/providers.json
apiVersion: config.delonix.io/v1
defaultProvider: libvirt
providers:
  - type: libvirt
  - type: opnsense
    url: https://fw.example
    auth: { keyFile: /etc/delonix/opnsense.key, secretFile: /etc/delonix/opnsense.secret }
networkDefaults:
  gateway: opnsense
```

## NÃO estável — pode mudar em qualquer versão

* **`serve cri`, `serve api`, `serve node-api`, `serve docker-api`** — superfícies de protocolo
  em construção. O `docker-api` publica a sua cobertura em
  `delonix serve docker-api --matrix`, e é essa tabela que diz o que existe
  hoje, não esta promessa. A **API de gestão** (`serve api`) é local (socket
  unix, só o próprio uid) e não tem contrato publicado: não construas automação
  sobre ela — para isso existe a CLI, com `-o json`. As rotas dela estão
  **congeladas** (só correcções) enquanto o contrato local de nó que a substitui
  é decidido (ADR-0040 e ADR-0041, ambos *Proposed*); uma promessa para esse
  contrato só entra nesta página quando a primeira vaga dele fechar.
  As rotas que mudavam a rede **sem o registo do container** —
  `/v1/net/firewall/:ip`, `/v1/net/egress[/:bridge]`, `/v1/net/attach-extra…` e
  `DELETE /v1/net/attach/:id/:ip` — passaram a responder **`501` com
  `DX-6302`**: o reapply seguinte desfazia o que escreviam, e só cobriam o IP
  primário de um container com várias redes. O caminho é a CLI (`net ingress`,
  `net egress`, `network connect`/`disconnect`), que passa pelo registo.
* **O nome do executável `delonix` é deste motor.** O control plane deixa de
  produzir um binário com esse nome (ADR 0038 do control plane); um `delonix`
  instalado por este projecto é sempre a CLI descrita nesta página.
* **`compatibility`** — a mesma tabela do `serve docker-api --matrix`, mas
  como verbo de topo e com `-o json`. Ainda só cobre `docker`; `compose`/
  `cri`/`oci` são superfícies futuras, cada uma com o seu próprio "três
  estados" por construir. Sem promessa de campos até `compose`/`cri`/`oci`
  entrarem — o formato pode mudar de forma para os acomodar.
* **`provider`** — `ls`/`describe`/`matrix`: o que cada provider (libvirt,
  cloud-hypervisor, proxmox, linux, opnsense) declara contra o catálogo de
  capacidades (ADR-0050, 1.1.0 desde o ADR-0059), medido no host. Os tipos são
  `compute`, `network`, `storage`, `image` e `gateway`; um `gateway` responde às
  linhas de rede. Os NOMES das capacidades (`vm.snapshot.memory`,
  `net.namespace-isolation`, …) e os seis estados são o que um script lê, e
  seguem a versão do catálogo (`catalog_version` no JSON): uma entrada nova sobe
  o minor, renomear ou remover sobe o major. As colunas da tabela e a forma do
  JSON além desses campos podem ainda mudar.
* **`cluster`, `vm`, `pod`, `workload`, `net`, `systemcontainer`** —
  a superfície ainda está a assentar. (O *schema* de `kind: Pod` é estável, ver
  acima; o que não é estável é o grupo de comandos `delonix pod`.)
  > **`storage`/`sharevolume` deixaram de ser grupos** (#216, «fold
  > storage/sharevolume into volume») — esta secção continuou a nomeá-los
  > durante um tempo depois de já não existirem, o que é exactamente o erro
  > que a lista mais abaixo existe para impedir (um nome classificado que já
  > não é um grupo real). A superfície deles vive hoje em `delonix volume`.
* **`volume`, `network`, `secret`** — só a saída `-o json` e (para `Volume`/
  `Network`) o schema do manifesto têm promessa (ver as duas secções
  acima); o resto da superfície imperativa (`create`/`rm`/`inspect` e as suas
  flags) nunca foi revisto para uma promessa própria. `volume` em particular
  acabou de absorver `storage`/`sharevolume` (#216) — está literalmente a
  meio de assentar, o pior momento possível para prometer nada sobre ele.
* **`apply`, `plan`, `wait`, `stack`, `manifest`, `get`, `describe`,
  `delete`, `diff`, `drift`** — o ciclo de reconciliação nativo (IaC, v0.47.0+). O
  SCHEMA dos manifestos que estes comandos consomem já é estável (secção
  acima); os VERBOS e flags destes comandos em si (`--detailed-exitcode`,
  `--prune`, `--replace`, …) nunca tiveram revisão própria, e a Kind mais
  recente (`Service`) ainda está a ganhar wiring nalguns destes caminhos.
* **`migrate`** — `migrate assess` lê um ficheiro compose que já tens e diz o
  que este motor serve, recusa e não implementa. É um RELATÓRIO (não cria, não
  puxa, não corre), e a tabela por trás dele é a do `compatibility compose` —
  uma medição com data, que se move quando o motor ganha chaves.
* **`compose`** — suporte nativo a `docker-compose.yml`, ainda a ganhar
  chaves do Compose Spec (`profiles`, `configs`/`secrets`, multi-ficheiro —
  ver o `AGENTS.md` para a lista completa do que falta).
* **`system`, `dashboard`, `completion`, `init`, `man`, `config`,
  `explain`, `api-resources`, `version`** — utilitários/introspecção, sem
  promessa declarada em nenhuma versão.
* **`hosts`** — `hosts sync` publica no `/etc/hosts` os nomes de serviço dos
  containers expostos (ADR-0048); superfície nova, sem promessa ainda.
* **`mcp`** — o servidor Model Context Protocol (ADR-0025), superfície nova. O
  transporte `stdio` é o suportado (um processo filho do cliente de IA por
  sessão, nunca um daemon); um transporte HTTP local, se vier a existir, seria
  loopback-only com um token local, como o `serve api` já é local-only e sem
  contrato publicado — não construas automação sobre ele ainda.
* **`policy`** — `delonix policy unset` (M04, `docs/roadmap/
  13-improvements-traceability.md`), o único verbo imperativo de
  `kind: RuntimePolicy`. Superfície nova, sem promessa declarada — o
  `policy.json` que ele apaga já existia e já era lido por `container run`/
  `vm create`; o que é novo é a forma declarativa (`stack apply`) e este
  comando de remoção. A leitura passa por `delonix get runtimepolicies`/
  `delonix describe runtimepolicy <nome>`, os verbos genéricos, que herdam a
  promessa (ou falta dela) desses verbos e não uma própria deste grupo.
* **`backup`** — o grupo não estava declarado de nenhum dos lados, e essa
  omissão é ela própria um defeito: quem quisesse saber se podia depender de
  `delonix backup vm x` não tinha resposta. Fica NÃO estável enquanto os
  arquivos ganham verbos. O **formato do arquivo** é outra coisa e tem a sua
  própria guarda: o `backup.json` traz um número de versão e um leitor que não o
  conheça RECUSA em vez de adivinhar uma disposição que nunca viu.

  **Mudou na v0.67.0:** `delonix backup <kind> <nome>` passou a
  `delonix backup create <kind> <nome>`, e o `delonix restore` de raiz passou a
  `delonix backup restore <arquivo>` — sem o `<kind>` posicional, que o arquivo
  já regista (continua a poder afirmar-se com `--kind` para que uma discordância
  seja recusada). O agendamento saiu de flags do backup para
  `delonix backup schedule`, com a mesma semântica: instala o temporizador **e**
  tira o primeiro arquivo. Corte limpo, sem aliases — a forma antiga falha com
  «unrecognized subcommand», nunca em silêncio.

  **`delonix system backup` não foi tocado nessa data.** É outro âmbito — o
  state root inteiro de um nó — e não uma segunda porta para este grupo.

  **Mudou depois (Sprint 4 da reestruturação da CLI, 2026-09-12):** o próprio
  `system backup`/`system restore` foi renomeado para `system snapshot
  create`/`system snapshot restore` — mesmo vocabulário de `vm snapshot`/
  `volume snapshot` (captura num instante), só que à escala do nó. O ÂMBITO
  não mudou nem se fundiu com este grupo — só a PALAVRA partilhada («backup»)
  desapareceu, que era a fonte real da confusão que o parágrafo anterior já
  andava a explicar por escrito. Corte limpo: a forma antiga falha com
  «unrecognized subcommand».
* **Tudo o que começa por `net netns`** — plumbing interno exposto por
  conveniência de depuração.
* **O formato dos ficheiros de estado** em `$DELONIX_ROOT`. Lê-se pelo `inspect`,
  nunca do disco.
* **`stack history`**, **`stack rollback`** e o conteúdo de `$DELONIX_ROOT/stacks/` (ADR-0019). É um
  registo do que foi aplicado, e o desenho promete explicitamente que **nada o
  lê para decidir o que existe**: apagar essa pasta não muda o que o `plan`, o
  `apply`, o `prune` ou o `destroy` fazem — só perde o histórico, e há gate na
  bateria E2E a exigi-lo. Automação que dependa da presença de uma revisão está
  a apostar contra essa promessa.

## Como uma quebra é feita, quando tem de acontecer

Precedente já cumprido pelo projecto: a reorganização da v0.30.0 (`netns` →
`net netns`, `cri` → `serve cri`, …) foi um **corte limpo, sem aliases** — a
forma antiga falha com «unrecognized subcommand», nunca em silêncio.

Isso mantém-se como regra: **falhar alto**. Um alias de compatibilidade que
muda de comportamento é pior que um erro.

E uma lição que custou esta sessão: essa quebra deixou um chamador INTERNO por
actualizar (o `delonix-cri` continuou a invocar `delonix netns attach`), o que
partiu a criação de pod rootless durante meses — ver
[cri-conformance.md](cri-conformance.md). Um corte limpo obriga a fazer o grep
por chamadores em TODO o workspace, não só na documentação.
