Skip to content

Capabilities Framework — Autonomia da Zelor no Front

Como o agente (Zelor) dirige a SPA: o catálogo declarativo de capabilities que o front publica para a IA e executa quando ela emite uma intent.

Esta doc complementa code-style.md e routing.md. Em caso de conflito, code-style.md vence.

O que é uma capability

Uma capability é um ponto de autonomia: uma ação que a Zelor pode executar na UI (navegar para uma tela, preencher um formulário, submeter, destacar um elemento). O front mantém um catálogo dessas capabilities, publica um resumo para a ai-api (via BFF) e, quando a IA escolhe uma e emite uma intent, o front a executa de verdade.

O conceito já existia espalhado em arquivos *.capability.ts por módulo. A arquitetura atual centraliza tudo em src/capabilities/** separando dado de comportamento.

Split dado vs comportamento (a ideia central)

Cada capability tem duas metades de naturezas diferentes:

MetadeO que éOnde viveSerializável?
Manifestometadado declarativo: key, rota, label, descrição, parâmetros, superfície*.ymlSim
Mappercomportamento: mutar store, abrir modal, submeter*.mapper.tsNão (closure ligado a stores/router)

O manifesto é o que a IA lê para escolher. O mapper é código vivo que toca useXxx() — não cabe em YAML. Por isso são arquivos separados, casados por convenção de path.

Estrutura de pastas

OBRIGATÓRIO: a árvore de src/capabilities/modules/ espelha onde a Zelor toma autonomia<módulo>/<feature>/<interação>.

src/capabilities/
├── schema.ts      # tipos do manifesto + defineCapabilityMapper()
├── loader.ts      # resolução de aliases, required, build da capability
├── index.ts       # glob dos .yml + .mapper.ts, valida, registra (boot)
└── modules/
    └── <módulo>/
        └── <feature>/
            ├── <interação>.yml          # manifesto (sempre)
            └── <interação>.mapper.ts    # comportamento (só se houver)

Exemplos reais:

modules/teams/create/create.yml + create.mapper.ts        # team.add-user
modules/workflows/list/filters.yml                         # workflow.filter-status (só navega/filtra)
modules/templates/structure/node/rename.yml + .mapper.ts   # template.structure-rename-node
RegraDescrição
PROIBIDO registrar capability fora de src/capabilities/O sistema antigo (src/modules/*/agente/*.capability.ts) foi 100% removido
OBRIGATÓRIO uma capability por ponto de autonomiaListar, filtrar, criar, editar, excluir são capabilities distintas
OBRIGATÓRIO .mapper.ts ao lado do .yml de mesmo basenameO loader casa por path; nome divergente não registra comportamento
Capability só de navegação/filtro pode ser .ymlSem apply/submit/highlight, dispensa mapper

O manifesto (.yml)

yaml
key: workflow.create            # identificador único <módulo-singular>.<ação>
module: workflows               # módulo dono
route: new-workflow             # name da rota no vue-router (tem que existir)
listRoute: list-workflow        # opcional: rota de listagem para "passar por" antes
label: Criar workflow           # rótulo curto PT-BR
description: Cria um novo workflow de aprovação, define nome e descrição e salva como rascunho
surface: WRITE_API              # READ | GUIDED | WRITE_API
actions: [navigate, fillForm, submit]
plan: null                      # opcional: chave de gating (ex: providerQtd)
params:                         # só quando há fillForm
  - field: name
    type: string                # string | number | boolean | date | enum
    required: true
    aliases: [nome, title, titulo]
    message: Nome do workflow não informado
  - field: description
    type: string
    required: false
    aliases: [descricao]
flow: [workflow.create, workflow.publish]   # opcional: sequência multistep

Campos

CampoObrigatórioDescrição
keyÚnico no catálogo. Padrão <módulo-singular>.<ação> (workflow.create, template.structure-rename-node).
moduleMódulo dono.
routename da rota no vue-router. Tem que existir — rota inexistente vira denied em runtime.
listRouteRota de listagem que o autopilot "visita" antes de navegar (efeito de demo).
labelRótulo curto, PT-BR.
descriptionRica, PT-BR. A IA escolhe a capability por aqui — descreva o efeito e os parâmetros.
surfaceSuperfície de execução mínima: READ (navegar/filtrar/buscar/destacar), GUIDED (preencher form assistido), WRITE_API (criar/editar/excluir/submeter).
actionsSubconjunto de navigate, fillForm, submit, highlight.
planChave de gating de plano, quando a tela é gateada. Default null.
paramsSó com fillForm. Cada um: field, type, required, aliases, message, options (enum).
flowSequência ordenada de keys para fluxos multistep.

O mapper (.mapper.ts)

ts
import { defineCapabilityMapper } from '@/capabilities/schema'
import { useWorkflowEditor } from '@/modules/workflows/hooks/useWorkflowEditor'

export default defineCapabilityMapper({
  apply: (values) => {
    const editor = useWorkflowEditor()

    if (!editor.flowSteps.length) {
      editor.initializeSteps()
    }

    editor.workflowName = values.name as string

    if (values.description) {
      editor.workflowDescription = values.description
    }

    return { status: 'ok' }
  },
  submit: async () => {
    const editor = useWorkflowEditor()

    await editor.saveWorkflow()

    if (!editor.workflowId) {
      return { status: 'denied' }
    }

    return { status: 'ok' }
  }
})

Contrato

SlotAssinaturaPapel
apply(values) => ResultAplica os valores: muta store, abre modal, aplica filtro. Chamado pela ação fillForm.
submit() => ResultComita (await store.save()). Chamado pela ação submit.
highlight() => voidSó visual.
validate(values) => FieldIssue[]Regras custom (CPF, formato). Devolve issues; vazio = ok.

Result é { status } com status ∈ { ok, needs_user_input, denied, awaiting_human_confirm, correctable }. Quando needs_user_input, acompanha fields: [{ field, rule, message }].

Regra crítica: valor vai em apply, submit é no-arg

OBRIGATÓRIO: ação que carrega valor (renomear um nó, definir uma condição, escolher um tipo) vai em apply(values) — que o fillForm invoca com os parâmetros já resolvidos. submit() não recebe argumentos; é só o commit final.

ts
// ERRADO — submit não recebe values
submit: (values) => structure.updateNodeTitle(values.nodeId, values.title)

// CERTO — comportamento com valor é apply (e a .yml declara actions:[fillForm] + params)
apply: (values) => structure.updateNodeTitle(values.nodeId as string, values.title as string)

O que o loader já faz por você (não repita no mapper)

ResponsabilidadeQuem faz
Resolver aliasesfield e dar trimLoader (a partir de params)
Checar required e devolver needs_user_inputLoader (a partir de params)
Regras custom (CPF, etc.)validate no mapper
Mutação de estado / commitapply / submit no mapper

PROIBIDO rechecar required dentro do mapper — values já chega resolvido e validado.

O loader e a integração com a IA

src/capabilities/index.ts faz o boot no import (chamado por src/main.ts via import '@/capabilities'):

  1. import.meta.glob('./modules/**/*.yml') e ('./modules/**/*.mapper.ts') — carrega tudo (eager).
  2. Casa .yml.mapper.ts pelo path-base; valida o manifesto.
  3. buildAgenteCapability(manifest, mapper) monta o objeto e chama registerUICapability de @/domain/agente.

YAML é carregado pelo plugin @modyfi/vite-plugin-yaml (registrado em vite.config.ts); *.yml tem declare module em src/types/capabilities-yaml.d.ts.

OBRIGATÓRIO: o framework reusa o runtime existente (registerUICapability, useAgenteDriver, summarizeUICapabilities). O contrato com BFF/ai-api não muda — o front continua publicando apenas { key, routeName }[] em POST sessions/:id/ui/capabilities. O metadado rico (description, params, surface) fica no front para escolha local e documentação.

Parâmetros de rota

navigate monta os params lendo os :tokens do path da rota e buscando cada nome no data da intent. Ou seja: se a rota é integrations/:id, a IA precisa mandar data: { id: '<connectionId>' } — os aliases do manifesto valem para fillForm, não para os params da rota.

A exceção é o companyId, que é contexto do tenant e não algo que a IA descobre: quando ele não vem no data, o driver herda o valor da rota atual (CONTEXT_PARAMS em useAgentDriver). Nenhum outro param é herdado, para não navegar com o id errado.

Ciclo de uma intent

IA escolhe capability  →  emite intent { capability, action, data }


useAgenteDriver.executeIntent
        ├─ navigate  → router.push(route) (passa por listRoute no studio)
        ├─ fillForm  → loader resolve values → required/validate → mapper.apply(values)
        ├─ submit    → mapper.submit()
        └─ highlight → mapper.highlight()


devolve { status } para a IA

Como adicionar uma capability nova

  1. Identifique o ponto de autonomia e a rota (name: no router do módulo).
  2. Crie src/capabilities/modules/<módulo>/<feature>/<interação>.yml com o manifesto.
  3. Se houver comportamento, crie o .mapper.ts de mesmo basename. Leia o hook real antes (src/modules/<m>/hooks/useXxx.ts) e use só membros que existem — nunca invente método.
  4. key única; rode os gates: npm run type-check, npm run lint, npm run build.

Regras de code-style (lint estrito)

Os mappers seguem o code-style.md como qualquer arquivo do front:

Regra
PROIBIDO comentáriosZero comentários nos .yml e .mapper.ts
PROIBIDO for/whileUsar .map/.filter/.reduce/.forEach
PROIBIDO try/catchDeixar o interceptor/exception filter tratar
PROIBIDO Date cruUsar @/utils/date
Imports só via @/Sem ../ profundo
Aterramento obrigatórioToda chamada de store/hook tem que existir de verdade

Funções helper no escopo do módulo do mapper são permitidas (o mapper não é hook nem componente).

Estado atual

113 capabilities em 8 módulos:

MóduloCapsCobertura
templates57create/list/edit/row/status + editor de estrutura (49): node, variable, signer, layout, library, preview, docx
revision-workflows18workflow de aprovação (distinto do builder): lista, filtros, timeline/nodes, condições, reviewer, status, publicar, excluir
workflows14builder de workflow: create/list/edit/steps/status
teams8membros, perfis de permissão, status
reports7create/list/filtros/download/delete
contracts5fluxo multistep: create/workflow/template/parties/review
compliance2create/add-step
profile2idioma, tema

Relacionados