Blueprint
O que é
Blueprint é o sistema de schemas declarativos do Hypergestor Integrations. Cada integração possui um arquivo .blueprint.yaml que serve como fonte única de verdade para IA, MCP, desenvolvedores e documentação.
A partir desse arquivo, geradores produzem automaticamente: contratos de validação, clients HTTP tipados, flowcharts de mapeamento, specs OpenAPI e tools MCP.
Motivação
Sem Blueprint, cada integração hardcoda credenciais, config, endpoints e mapeamentos. Isso causa:
- Inconsistência: cada dev implementa de forma diferente
- Opacidade para IA: modelos não conseguem entender o pipeline sem ler todo o código
- Duplicação: mesma informação repetida em tipos, docs, testes e configs
- Fragilidade: mudanças na API externa quebram silenciosamente sem validação
Blueprint resolve centralizando tudo em um YAML declarativo com geradores automáticos.
Estrutura do Blueprint
Cada integração possui um arquivo co-localizado:
hub/connectors/demo/
demo.blueprint.yaml # Fonte única de verdade
specs/
demo-api.yaml # OpenAPI spec da API externa
_gen/ # AUTO-GENERATED (pnpm blueprint:generate)
contracts.ts # Zod schemas + TS types
client.ts # createDemoClient() tipado
base-server.ts # BaseServer + Handler classes
handlers/ # Handler base classes
flow.md # Flowchart Mermaid dos handlers
openapi-internal.yaml # OpenAPI 3.1 — API interna (collect/process, execute)
openapi-external.yaml # OpenAPI 3.1 — API externa (endpoints do integrador)
mcp.json # MCP tools + resources (opcional)
e2e.test.ts # Testes e2e gerados
src/
server.ts # Estende BaseServer
handlers/ # Concretes dos handlers
Schema YAML
# hub/connectors/demo/demo.blueprint.yaml
name: Demo
key: demo
icon: flask
description: Integração de demonstração para testes e referência
openapi: ./specs/test-api.yaml
credentials:
- name: api_key
label: API Key
type: string
required: true
sensitive: true
- name: api_secret
label: API Secret
type: string
required: true
sensitive: true
config:
- name: base_url
label: URL Base
type: string
required: true
default: 'https://api.demo.com/v1'
handlers:
inbound:
get-leads:
description: Busca leads da API Demo para o CRM
trigger: pulling
schedule: 5m
endpoints: [get-leads]
working_data:
- name: lead_id
type: string
map: id
- name: customer_name
type: string
map: nome_completo
- name: lead_source
type: string
map: custom # requer implementação manual no handler
log_data:
- name: lead_id
map: id
- name: customer_name
map: nome_completo
list-vehicles:
description: Lista veículos disponíveis na API Demo
trigger: gateway
endpoints: [list-vehicles]
get-vehicle:
description: Busca detalhes de um veículo na API Demo
trigger: gateway
endpoints: [get-vehicle]
get-lead-status:
description: Consulta status de um lead na API Demo
trigger: gateway
endpoints: [get-lead-status]
outbound:
send-lead-update:
description: Envia atualização de lead do CRM para a API Demo
trigger: pulling
schedule: 10m
endpoints: [update-lead]
working_data:
- name: full_name
type: string
map: custom
- name: email
type: string
map: email
- name: phone
type: string
map: phone
log_data:
- name: full_name
map: custom
endpoints:
inbound:
get-leads:
openapi: GET /leads
list-vehicles:
openapi: GET /vehicles
get-vehicle:
openapi: GET /vehicles/{id}
get-lead-status:
openapi: GET /leads/{id}/status
outbound:
update-lead:
openapi: PUT /leads/{id}
Campos do Schema
| Campo | Descrição |
|---|---|
name | Nome de exibição da integração |
key | Identificador único (kebab-case) |
icon | Ícone para UI |
description | Descrição da integração |
credentials | Campos sensíveis (API keys, secrets) — marcados com sensitive: true |
config | Configurações operacionais (URLs, limites) — com default opcional |
port | Porta HTTP do serviço da integração |
handlers | Organizados por direção (inbound / outbound), cada um com trigger, schedule, endpoints, working_data e log_data |
endpoints | Organizados por direção (inbound / outbound), cada um com url, method, headers, request/response |
Trigger
O campo trigger define o modo de operação do handler — como ele é ativado.
| Valor | Descrição | Pipeline |
|---|---|---|
pulling | Busca dados ativamente no schedule (polling) | collect → process (transform interno) |
listener | Escuta eventos externos (webhook) — não tem collect, recebe dados passivamente | process (transform interno) |
gateway | Proxy síncrono — recebe request, chama API externa, retorna resposta | execute → transform (interno) |
Handlers pulling requerem schedule. Handlers listener não têm schedule nem rota de collect.
Schedule
O campo schedule define o intervalo mínimo entre execuções de um handler pulling. O scheduler roda a cada 1 minuto e enfileira handlers cuja última execução foi há mais tempo que o schedule definido.
| Formato | Exemplo | Comportamento |
|---|---|---|
| Intervalo | 5m, 10m, 1h | Roda quando última execução >= intervalo |
| Horário fixo | "08:00" | Roda todos os tenants naquele horário |
| Múltiplos horários | "08:00,16:00" | Roda em cada horário listado |
O schedule é propriedade da integração/handler, não do tenant. Todos os tenants com o handler habilitado seguem o mesmo schedule.
Direção dos Endpoints
| Direção | Significado | Exemplo |
|---|---|---|
inbound | Integrador → Nós (buscamos dados do sistema externo) | GET leads da API Demo |
outbound | Nós → Integrador (enviamos dados para o sistema externo) | PUT lead para API Demo |
Field Mapping
Cada campo de working_data, request e response possui:
| Propriedade | Descrição |
|---|---|
name | Nome ou path do campo (suporta . para nested e [] para arrays) |
type | Tipo: string, number, boolean, date, object, array |
map | Nome do campo de origem, ou custom para mapeamento manual no código |
mock | Valor mock opcional para testes e modo simulação |
nullable | true se o campo pode vir null — gera .nullable() no Zod, | null em TS |
hidden | true para omitir o campo quando usado como override de endpoint openapi: (ver §) |
Campos com map: custom requerem implementação manual no handler — o gerador produz um placeholder/TODO.
Notação de paths em Response
Campos de response suportam . para nesting de objetos e [] para arrays em qualquer nível. O parser trata ambos simetricamente — não é necessário declarar containers, só as folhas:
# Response é um array de objetos (API retorna [{id, nome}, ...])
response:
- name: '[].id'
type: string
- name: '[].nome'
type: string
# Objeto nested (não precisa declarar 'customer' separado)
response:
- name: customer.email
type: string
- name: customer.address.city
type: string
# Objeto com um campo que é array (não precisa declarar 'results[]')
response:
- name: total
type: number
- name: results[].id
type: string
# Arrays aninhados
response:
- name: items[].id
type: string
- name: items[].contacts[].phone
type: string
# Mix livre
response:
- name: '[].cliente.codigo'
type: number
- name: '[].servicos[].valor'
type: number
nullable: true
Regras:
name: 'campo'— leaf simples no objeto responsename: 'a.b'—bvira folha dentro dea(objeto nested inferido)name: 'campo[]'— array de objetos (container; raramente necessário declarar sozinho)name: 'campo[].sub'—subé campo de cada item do arraycampo(suficiente, sem precisar do container)name: '[]' / '[].campo'— a raiz da response é um array- A composição é recursiva e simétrica:
items[].contacts[].phoneoucustomer.address.cityouitems[].cliente.codigo - O primeiro separador que aparece no path decide o nível —
a.b[].cnesta como objetoa→ arrayb→ folhac
O gerador produz tipos separados para arrays raiz:
// Para response com [] raiz:
export const GetLeadsResponseItemSchema = z.object({ id: z.string(), ... });
export type GetLeadsResponseItem = z.infer<typeof GetLeadsResponseItemSchema>;
export const GetLeadsResponseSchema = z.array(GetLeadsResponseItemSchema);
export type GetLeadsResponse = z.infer<typeof GetLeadsResponseSchema>;
Tipagem dos Handlers
Os tipos CollectResponse, TransformResponse e ProcessResponse do @hg/core suportam generics:
CollectResponse<TPayload = unknown> // items[].payload tipado
TransformResponse<TMapped = unknown> // mapped tipado
ProcessResponse // sucesso/erro (sem generic)
O gerador usa os tipos do blueprint para tipar os base handlers automaticamente:
| Handler | Método | Return Type | Body Cast |
|---|---|---|---|
| Inbound | collect | CollectResponse<EndpointResponseItem> | — |
| Inbound | transform | TransformResponse<WorkingData> | payload: EndpointResponseItem (chamado internamente pelo /process) |
| Outbound | collect | CollectResponse (payload do CRM, sem tipo) | — |
| Outbound | transform | TransformResponse<WorkingData> | payload: Record<string, unknown> (chamado internamente pelo /process) |
| Ambos | process | ProcessResponse | Recebe { payload }, aplica transform internamente, depois processa com { mapped } |
| Gateway | execute | GatewayResponse | { query, body } — retorno passa por transform antes de devolver |
Geradores
Contracts (pnpm blueprint:generate:contracts)
Gera schemas Zod + tipos TypeScript para cada endpoint, handler e config:
// generated/contracts.ts (AUTO-GENERATED)
import { z } from 'zod';
export const DemoCredentialsSchema = z.object({
api_key: z.string(),
api_secret: z.string(),
});
export type DemoCredentials = z.infer<typeof DemoCredentialsSchema>;
// Endpoint com response [] gera Item + array
export const GetLeadsResponseItemSchema = z.object({
id: z.string(),
nome_completo: z.string(),
email: z.string(),
telefone: z.string(),
});
export type GetLeadsResponseItem = z.infer<typeof GetLeadsResponseItemSchema>;
export const GetLeadsResponseSchema = z.array(GetLeadsResponseItemSchema);
export type GetLeadsResponse = z.infer<typeof GetLeadsResponseSchema>;
export const GetLeadsWorkingDataSchema = z.object({
lead_id: z.string(),
customer_name: z.string(),
lead_source: z.string(),
});
export type GetLeadsWorkingData = z.infer<typeof GetLeadsWorkingDataSchema>;
Client (pnpm blueprint:generate:client)
Gera factory function com métodos tipados, mapping automático e suporte a mock:
// generated/client.ts (AUTO-GENERATED)
export function createDemoClient(options: { credentials: DemoCredentials; config: DemoConfig; mock?: boolean }) {
return {
async getLeads() {
/* ... */
},
async updateLead(data: UpdateLeadRequest) {
/* ... */
},
async listVehicles() {
/* ... */
},
async getVehicle(id: string) {
/* ... */
},
async getLeadStatus(id: string) {
/* ... */
},
};
}
- Métodos derivados dos endpoint keys:
get-leads→getLeads() - Template interpolation:
{{credentials.api_key}}resolvido em runtime - Validação Zod em requests e responses
mock: trueretorna dados dos camposmockdo schema
Flow (pnpm blueprint:generate:flow)
Gera flowcharts Mermaid por handler mostrando o pipeline completo:
OpenAPI (pnpm blueprint:generate:openapi) — opcional
Gera spec OpenAPI 3.1 descrevendo a API externa que a integração consome.
Override de fields via OpenAPI spec
Quando um endpoint usa openapi: METHOD /path, o request e response são resolvidos a partir do spec. Se o spec está errado (tipo trocado, campo faltando, campo que não queremos expor), declare só os ajustes no YAML — o merge é patch por name literal, não substituição.
openapi: ./specs/foo-api.yaml
endpoints:
inbound:
get-agendamento:
openapi: GET /v1/agenda/{codEmpresa}
response:
- name: 'servicos[].valor'
type: number # corrige tipo trocado no spec
- name: 'cliente.cpf'
hidden: true # omite do output
- name: 'cliente.telefone'
nullable: true # spec não marcou mas a API devolve null
- name: 'extra_field'
type: string # campo que o spec esqueceu — adiciona
Regras do merge:
- Match por
name(literal, mesma grammar das folhas —cliente.codigo,servicos[].valor,results[].customer.email): chaves declaradas sobrescrevem as do spec; chaves omitidas preservam o resolvido. - Sem match no spec: o field é adicionado, silenciosamente.
hidden: true: remove o field. Cascata implícita —hidden: trueemclientetambém removecliente.codigo,cliente.nome,cliente.endereco.rua, etc.
Isto só vale quando o endpoint tem openapi:. Sem a ref, request/response continua sendo a definição completa e obrigatória.
Null-safety
Quando o spec declara nullable: true (OpenAPI 3.0) ou type: ['string', 'null'] (3.1), o gerador propaga automaticamente: Zod emite .nullable(), TS emite | null. Se o spec está incompleto, marque manualmente no override com nullable: true.
openapi_fixes — normalização do spec
Para specs com malformações estruturais comuns, há fixes opt-in no root do blueprint:
openapi: ./specs/foo-api.yaml
openapi_fixes:
- untyped_array_items
untyped_array_items— trata schema comitemsmas semtypecomotype: array; desfaz o idiom invertido{ items: { type: "array", oneOf: [ref] } }(observado no spec da NBS).
Cada fix aplicado sai como warning no console com o path exato, tornando o patching visível e escopado por integração.
MCP (pnpm blueprint:generate:mcp) — opcional
Gera definições MCP (Model Context Protocol):
- Tools por endpoint:
demo_get_leads,demo_update_lead,demo_list_vehiclescom input/output schemas - Resources com mapeamentos: metadata dos field mappings por tool
- Flow resource (
demo_describe_flow): flowchart completo por handler — permite que a IA entenda o pipeline, identifique campos custom e debugue problemas de dados sem ler código
Comandos
| Comando | Descrição |
|---|---|
pnpm blueprint:validate | Valida todos os .blueprint.yaml |
pnpm blueprint:generate | Gera contracts + client + flow (padrão) |
pnpm blueprint:generate:contracts | Gera apenas Zod schemas + types |
pnpm blueprint:generate:client | Gera apenas client HTTP tipado |
pnpm blueprint:generate:server | Gera apenas BaseServer + Handler classes |
pnpm blueprint:generate:flow | Gera apenas flowcharts Mermaid |
pnpm blueprint:generate:openapi | Gera apenas OpenAPI spec |
pnpm blueprint:generate:mcp | Gera apenas MCP tools + resources |
pnpm blueprint:test | Testa todas as integrações (sobe mocker) |
pnpm blueprint:test demo | Testa uma integração específica |
pnpm blueprint:mocker | Sobe mock server da API externa |
CLI descobre blueprints via glob hub/connectors/*/*.blueprint.yaml ou aceita path específico. Aceita nome curto (ex: demo resolve para hub/connectors/demo/demo.blueprint.yaml).
Server (pnpm blueprint:generate:server)
Gera classes base para handlers e servidor Fastify:
// .gen/handlers/base-get-leads.ts (AUTO-GENERATED)
import type { CollectResponse, TransformResponse, ProcessResponse } from '@hg/core';
import type { GetLeadsResponseItem, GetLeadsWorkingData } from '../contracts.js';
export abstract class BaseGetLeads {
http!: HttpClient;
endpoint!: DemoClient;
async collect(_req: FastifyRequest): Promise<CollectResponse<GetLeadsResponseItem>> {
return { items: [] };
}
async transform(req: FastifyRequest): Promise<TransformResponse<GetLeadsWorkingData>> {
const { payload } = req.body as { payload: GetLeadsResponseItem };
const mapped: GetLeadsWorkingData = {
lead_id: payload.id,
customer_name: payload.nome_completo,
lead_source: undefined!, // custom — requer override
};
return { mapped };
}
async process(_req: FastifyRequest): Promise<ProcessResponse> {
return { success: true };
}
}
O desenvolvedor estende as classes para customizar apenas o necessário:
// src/handlers/get-leads.ts
import { BaseGetLeads } from '../../.gen/handlers/base-get-leads.js';
export class GetLeads extends BaseGetLeads {
async collect(_req: FastifyRequest) {
const leads = await this.endpoint.getLeads();
return { items: leads.map((lead) => ({ externalId: lead.id, payload: lead })) };
}
// transform() usa auto-mapping da classe base
// process() customizado para gravar no CRM
}
Cada handler é uma classe com métodos collect, transform e process (standard) ou execute e transform (gateway). O transform() gerado aplica automaticamente os mapeamentos de campos definidos no blueprint (exceto campos map: custom, que requerem override manual). O transform não é um endpoint HTTP — é chamado internamente pelo serviço dentro da rota /process (standard) ou /execute (gateway). Os tipos fluem da base class — o override herda as assinaturas tipadas sem precisar de anotação explícita.
Package: @hg/blueprint
packages/blueprint/
src/
cli.ts # Entry point dos scripts pnpm
loader.ts # loadBlueprint() — parse YAML + validação
validator.ts # Meta-schema Zod que valida .blueprint.yaml
derive-meta.ts # deriveIntegrationMeta() — blueprint → IntegrationMeta
mocker.ts # Mock server Fastify que simula API externa
test-runner.ts # Test runner genérico — sobe mocker, testa handlers
test-reporter.ts # Formatação de resultados no terminal
generators/
contracts.ts # Gera Zod schemas + TS types
client.ts # Gera client HTTP tipado com mapping + mock
server.ts # Gera BaseServer + Handler classes
flow.ts # Gera flowchart por handler (Mermaid)
openapi.ts # Gera OpenAPI 3.1 spec
mcp.ts # Gera MCP tools + resources
utils/
response-tree.ts # Parse response fields com notação [] → árvore nested
interpolation.ts # Resolve {{credentials.x}}, {{config.y}}
type-mapping.ts # SchemaFieldType → Zod / TS / OpenAPI type
naming.ts # toPascalCase, toCamelCase
handlers.ts # flattenHandlers() — merge inbound/outbound
mock-builder.ts # buildMockPayload/Config/Credentials a partir do blueprint
runtime-schema.ts # Constrói Zod schemas em runtime para validação
Environments
Cada integração define seus environments no blueprint YAML. Environments permitem separar URLs, config e credentials por ambiente (homolog, prod, mock), com overrides por tenant.
Definição no Blueprint
environments:
environments:
- key: homolog
label: Homologação
default: true
urls:
api: 'https://homologacao.example.com/v1'
token: 'https://homologacao.example.com/token'
- key: prod
label: Produção
urls:
api: 'https://api.example.com/v1'
token: 'https://api.example.com/token'
url_fields:
- name: api
label: URL Base da API
- name: token
label: URL de Autenticação
configurable: true # permite o tenant sobrescrever URLs
Um environment mock é gerado automaticamente apontando para localhost:4900 (mock server compartilhado).
Resolução de Configuração
Quando o gateway ou runner executa um handler, URLs/config/credentials são resolvidos em 4 camadas (prioridade decrescente):
| Prioridade | Fonte | Local | Descrição |
|---|---|---|---|
| 1 | Override | hub_config_overrides (tenant) | Valores específicos por environment e tenant |
| 2 | Config base | hub_configs (tenant) | Valores base do tenant (flat, sem segmentação) |
| 3 | Blueprint YAML | *.blueprint.yaml → plt_types | Defaults por environment definidos no blueprint |
| 4 | OpenAPI spec | specs/*.yaml | URLs extraídas da spec (fallback estrutural) |
Merge por campo: override[campo] ?? config_base[campo] ?? blueprint_default[campo].
Banco de Dados
plt_types.environments(public) — definição de environments e url_fields, registrada viapnpm db:preparehub_configs.environment(tenant) — environment ativo do tenant, sem default (obrigatório selecionar)hub_config_overrides(tenant) — overrides por environment, permite múltiplos por env quandomultiple: true
Sem environment selecionado, gateway retorna 400.
Funções de Resolução (@hg/core)
resolveEnvironmentUrls(base, type, environment, override?)— merge URLsresolveEnvironmentConfig(base, type, environment, override?)— merge configresolveEnvironmentCredentials(base, type, environment, override?)— merge credentials
Mock Server
hub/mocker/ — sobe todos os connectors num único Fastify na porta 4900. Inclui endpoints OAuth token mock (POST /, /token, /v1/token) e registra rotas com e sem prefixo de slug. Iniciado via nx run @hg/hub-mocker:dev.
Integração com Arquitetura Existente
- Runner não muda — exceto passar
credentialseconfigno body das chamadas HTTP - IntegrationMeta derivado do blueprint via
deriveIntegrationMeta(blueprint)— embutido nogenerated/server.ts - BaseServer gerado registra rotas Fastify e instancia handlers —
src/server.tsestende e customiza - Handler classes geradas com
collect(),transform(),process()(standard) ouexecute(),transform()(gateway) — dev estende apenas o que precisa. Transform é método interno, não endpoint HTTP - Handlers usam o client gerado em vez de
fetchmanual - Contracts validam nas fronteiras (response da API externa, working_data no map)
- Integrations continuam stateless — credentials/config recebidos via request
Dependências
| Package | Onde | Motivo |
|---|---|---|
zod | @hg/core | Runtime validation dos contracts |
yaml | @hg/blueprint | Parse dos .blueprint.yaml |
glob | @hg/blueprint | Descoberta de blueprints |