Garantias de paridade de contratos do Service Management
Este é o documento E4 da cadeia de documentação E1–E8 do Service Management (JUM-479 ). Ele documenta o que as exportações de contrato do designer garantem — exatamente como o código se comporta hoje, após a entrega da frente de paridade de contratos (JUM-474 , JUM-475 , JUM-476 , JUM-477 , JUM-478 ) e de sua maquinaria de verificação (JUM-470 , JUM-471 ).
A garantia é uma promessa ao boilerplate, não uma lista de funcionalidades: um artefato exportado do designer pode ser consumido pelo boilerplate sem edição manual. Cada garantia abaixo nomeia a verificação que a comprova, para que o leitor possa verificar em vez de confiar — e para que um mantenedor que quebre uma delas reprove uma suíte, e não uma opinião em revisão.
A superfície de exportação em si (oito exportadores, seus formatos e o portão de qualidade de exportação) está fixada no Requisito 126, Contrato 3 — este documento aponta para ele em vez de duplicá-lo. Os passo a passo de uso estão em Funcionalidades e uso do Domain Designer (seções 10.1–10.2).
Garantia 1 — a exportação OAS 3.1 é consumível pelo boilerplate (JUM-474)
Construtor: buildOasDocument em
packages/designer-core/src/exporters/designerExporters.js.
O documento declara openapi: '3.1.0' — 3.1, não 3.0 — porque essa é a
versão que o boilerplate consome. O
spec/1.0.0.yml canônico declara 3.1.0, e o modelo
de entidades é legislado em OpenAPI 3.1 de ponta a ponta pelo
Requisito 026
(3.1 é a linha alinhada ao JSON Schema 2020-12; a exportação JSON Schema do
designer mira o mesmo draft). Uma exportação que correspondesse à sintaxe da
versão canônica, mas não à sua versão, seria um contrato que o consumidor
teria que traduzir — por isso a exportação emite exatamente o dialeto que os
portões do boilerplate já validam.
É garantido que o documento exportado satisfaz o
Requisito 036,
a disciplina de objetos de porta aplicada ao spec canônico por
ci-cd/check-oas-route-resolution.js:
operationIds de verbos canônicos. Toda operação carrega umoperationIdno esquema de verbos canônico (getAll*/create*/get*ById/update*/delete*), qualificado pelo nome do schema para que os ids permaneçam únicos entre domínios (getAllBilling_Invoice).- A disciplina de
$ref. Corpos de requisição referenciam os objetos de porta de entradaRequestCreate<Schema>/RequestUpdate<Schema>via$ref; toda resposta 2xx referencia o schema da entidade, seu wrapper<Schema>ArrayOfou oResourceDeleteResponsecompartilhado — nunca um schema inline. Todo schema referenciado carrega umadescriptionnão vazia. - Códigos de erro canônicos. As respostas de erro usam o conjunto de status de ERROR-CONTRACTS-AND-RESPONSES (400/401/403/404/409) com suas descrições canônicas.
- Os wrappers de porta são marcados, não escondidos. Os wrappers derivados
de entrada/saída de porta carregam
'x-port-object': true, o que permite ao importador OAS ignorá-los (ver Garantia 4).
Comprovado por:
designerOasCompliance.test.ts,
que importa validatePortObjectContracts e resolveSchemaByRef do
verificador real (não de uma cópia) e os aplica a um documento exportado de
um modelo no estilo da UI — a exportação não pode divergir do portão sem
reprovar a suíte. A metade da garantia referente a colisões de nomes (dois
nomes que tokenizam para o mesmo schema/rota, por exemplo Foo Bar vs
Foo-Bar, são erros que bloqueiam o portão de exportação em vez de sobrescritas
silenciosas) vive em
modelValidation.js
e é fixada por
modelValidation.test.ts.
Garantia 2 — AsyncAPI 3.0 por transporte e um proto canônico (JUM-475)
Construtores:
packages/designer-core/src/exporters/asyncApiExporters.js.
- Um arquivo por transporte, nomenclatura canônica. A exportação emite
<version>.websocket.ymle<version>.grpc.yml, correspondendo um a um ao diretóriospec/asyncapi/— nunca um documento combinado único, nunca o formato publish/subscribe 2.x. No 3.0, os canais carregammessagese o mapaoperationsde topo carregaaction: send|receive(contratosresponsesão recebidos; todo outro tipo é enviado), além de$refs de canal/mensagem. - A mesma disciplina de
$refde payloads do OAS. Os payloads de mensagem vivem uma única vez sobcomponents.schemase as mensagens os referenciam — payloads idênticos compartilham uma única entrada de schema em vez de serem inline por mensagem. - Todo documento exportado valida contra
validateAsyncApi30Document(asyncApi30Validation.js), o validador estrutural interno para o formato 3.0 (o repositório não depende de@asyncapi/parser). Os arquivos canônicos despec/asyncapi/passam pelas mesmas regras — paridade de formato drop-in entre o que o designer emite e o que o boilerplate entrega. - A exportação proto gRPC reproduz o envelope canônico. proto3, pacote
realtime, serviçoAsyncApiGateway, mensagens de envelopeAsyncApiRequest/AsyncApiResponse, um par rpc/mensagem por contrato de mensagem do designer. Para um modelo sem contratos, o proto emitido é byte a byte idêntico aospec/asyncapi/async-api.protoversionado.
Comprovado por:
designerAsyncApiExport.test.ts
— incluindo a asserção de identidade byte a byte e o teste que executa o
validador sobre os próprios arquivos canônicos, de modo que os documentos
canônicos e a exportação divergem juntos ou reprovam juntos.
Garantia 3 — o bundle de codegen é código entregável (JUM-476)
Construtor:
packages/designer-core/src/codegen/hexagonalCodegen.js,
consumido tanto pelo exportador de bundle de boilerplate
(buildBoilerplateBundleDocument, artefato kind: 'boilerplate-bundle',
version: '2.0.0') quanto pelo painel Code Preview do designer — mesmo
construtor, então preview e bundle não podem divergir.
- O layout é hexagonal e espelha o módulo Users migrado
(
src/modules/<Domain>/comdomain/{Entity,Model,security},application/{ports,use-cases},adapters/in/http/controllers,adapters/out/persistence,composition/eevents/contracts/apenas quando existem contratos de mensagem — ver HEXAGONAL-FEATURE-DRIVEN-MIGRATION). - Os formatos de contrato são consumidos, nunca rederivados. Os tipos de
campo vêm dos schemas de componente OAS da Garantia 1, as rotas HTTP de seus
paths e
operationIds, e os canais de evento dos canais AsyncAPI da Garantia 2. Uma mudança de contrato regenera o código; o código nunca bifurca o contrato. - A saída passa nas verificações de arquitetura do próprio repositório.
Todo controller gerado passa no
validateControllerFiledeci-cd/check-hexagonal-boundaries.js, e o grafo de imports gerado aponta apenas para dentro (domain ← application ← adapters ← composition). - A saída compila. O conjunto de arquivos emitido compila sob
tsc --strict— a suíte grava o bundle em um diretório temporário e executa o compilador real sobre ele.
O que a pessoa desenvolvedora ainda escreve. O bundle é um esqueleto
executável, não um serviço: o adaptador de persistência é um espelho em
memória (Map) de UserDataRepository.ts que você substitui pelo cliente de
armazenamento real, a raiz de composição precisa ser conectada ao bootstrap do
serviço, e qualquer regra de negócio além dos verbos CRUD canônicos é sua. A
garantia cobre a fronteira — layout, contratos, compilação, verificações de
arquitetura — não a lógica de aplicação.
Comprovado por:
hexagonalCodegen.test.ts.
Garantia 4 — fidelidade de ida e volta, com as fronteiras honestas (JUM-471, JUM-478)
Suíte:
designerRoundTrip.test.ts.
A propriedade sob teste é a própria travessia — exportar → importar →
comparar — de modo que um cancelamento do tipo exportador-omite-campo +
importador-ignora-campo não consiga se esconder atrás de saídas fixas
esperadas.
Travessias simétricas (sem perdas, deep-equal assertado)
- JSON (
buildJsonExportDocument→buildStateFromSuiteExportsobrenormalizeStatePayload): o documento versionado de suíte completa (JUM-547/JUM-736) —domains,relationships,view,interfaces,serviceConfiguration,codeWorkspaceedeploymentsfazem ida e volta com deep-equal, e a exportação é idempotente. A fronteira é documentada e assertada: seleções eidCounternão fazem parte do documento e são recomputados na importação, eruntimeEnvironmentatravessa apenas como a seleção de ambiente (veja a seção JUM-547 abaixo). Documentos anteriores ao JUM-547, só de domínio ({ domains, relationships, view }, semkind/version), importam normalmente com as seções ausentes preenchidas com padrões; um documento com seção desconhecida no nível raiz,versionmajor mais recente oukinddiferente deservice-management-suitefalha claramente em vez de importar pela metade. - Pacote de domínio (
buildDomainPackageDocument→buildDomainFromPackage): um pacote faz ida e volta com deep-equal em um modelo vazio, carimbado com proveniência (JUM-492): o documento v2 carrega um blocopackage({ name, version, dependencies }), e o conteúdo importado registracontext.provenance/meta.provenance({ package, version }). Reimportações são conscientes de versão: a mesma versão com conteúdo igual é no-op, a mesma versão com conteúdo diferente e downgrades são recusados, e uma versão mais nova faz merge determinístico — mudanças aditivas e de metadados são aplicadas, e remoções, estreitamentos, RBAC e invariantes mantêm o conteúdo existente e são listados na prévia de merge para decisão do usuário (Requirement 126 Contrato 3). Faixas de versão de dependências são resolvidas contra o registro de pacotes instalados; dependências ausentes ou incompatíveis são reportadas, e um ciclo que o pacote de entrada fecharia é recusado. A recomputação de ids do JUM-617 continua guardando o caminho de anexação (um pacote diferente com ids colidindo).
A travessia OAS: ponto fixo, lista de perdas vazia
O OAS é mais estreito que o modelo interno, então a travessia OAS era a com perdas. O JUM-478 reduziu a lista de perdas em nível de modelo de 21 caminhos de diff para zero, e a suíte asserta a lista vazia exata — um campo entrando (ou voltando) silenciosamente para ela reprova a suíte:
- Exportar → importar → exportar alcança um ponto fixo; o diff de
documento da primeira travessia é exatamente
[](assertado comtoStrictEqual, não inspecionado visualmente). - O que o OAS não consegue expressar nativamente atravessa como extensões
x-acordadas que o importador normaliza de volta paraentity.meta:x-aggregate-root,x-invariants,x-rbac(emitido apenas quando a política diverge do padrão do designer — umx-rbacausente normaliza de volta para exatamente esse padrão),x-fieldless: true(um conjunto de campos vazio sobrevive em vez de ganhar os campos padrãoid/createdAt/updatedAtdo importador) ex-field-flags: { pk, fk, unique }por campo (emitido apenas quando os flags divergem da heurística de nomes do importador:id→ PK/unique,*Id→ FK). Contratos de mensagem e composição (oneOf/allOf/anyOf,discriminator,x-external-refs) atravessam da mesma forma. - Relacionamentos atravessam como linhas
x-relationsde topo indexadas por nome de schema ({ name, fromSchema, toSchema, fromCardinality, toCardinality }) — nunca por id de modelo, porque o importador recomputa ids e ids no documento quebrariam o ponto fixo. Linhas cujos pontos de extremidade não importaram são descartadas, a mesma regra quenormalizeStatePayloadaplica a ids de modelo pendentes. - Os wrappers de objeto de porta são ignorados por design — e isso não é
uma perda.
RequestCreate*/RequestUpdate*/*ArrayOf/ResourceDeleteResponsesão artefatos derivados, não estado do modelo; reimportá-los fabricaria entidades fantasma. Documentos exportados pelo designer os marcam com'x-port-object': true.
As fronteiras que permanecem (nomeadas, assertadas, não varridas para debaixo do tapete)
Para documentos exportados pelo designer, a travessia em nível de modelo ainda não carrega, por design:
- Blocos de bounded context de domínio (
domain.context) e layout do canvas (posições, cores) — não têm transporte por extensão; a importação os recomputa. Os nomes de domínio e entidade sobrevivem. - A normalização de formato tipado: campos tipados (
uuid/date/datetime) retornam carregando o formato canônico que o exportador derivou do tipo. A suíte asserta isso como a transformaçãoapplyDocumentedOasFormatNormalization, de modo que é parte do contrato, não um acidente. - Estado de view (zoom, filtros, o toggle do portão de exportação) é conteúdo da exportação JSON, não do OAS — o importador OAS nunca o restaurou.
Para documentos estrangeiros — arquivos OAS que o designer não emitiu,
como o spec/1.0.0.yml canônico — o importador
reconhece objetos de porta por convenção (as mesmas regras de
nomeação/descrição: Request*, *ArrayOf, ResourceDeleteResponse,
descrições “Port input/output object” que não são contratos de entidade
<Name> resource, e schemas não-objeto), e as perdas remanescentes nomeadas
são:
- as facetas
example/default/minItems/maxItems— o modelo de campos do designer não tem espaço para elas; - vinculações
$refde itens de array — referências a objetos de valor são achatadas para o vocabulárioitemsType; operationIds legados (não canônicos) — a importação não preserva operationIds, então a reexportação os regenera no esquema de verbos canônico.
A própria importação canônica está fixada: o spec/1.0.0.yml (OpenAPI 3.1.0,
33 operationIds) importa como um domínio Imported com exatamente os seis
schemas de contrato (Document, Email, Address, Phone, User,
Organization) — sem entidades fantasma de objetos de porta, sem
relacionamentos — com normalização completa de meta, e reexporta para um
ponto fixo cujos schemas de entidade mantêm as propriedades e os conjuntos de
obrigatórios da origem, e cujos paths carregam as cinco operações CRUD
canônicas por schema.
Exportadores de mão única
Markdown, JSON Schema, AsyncAPI e o bundle de boilerplate não têm importador.
Suas suítes assertam invariantes estruturais em vez de uma travessia: todo
domínio/entidade/campo/relacionamento é renderizado; uma definição JSON Schema
por entidade com required ⊆ properties e additionalProperties: false; uma
operação AsyncAPI 3.0 por contrato por transporte com refs de payload
compartilhados; um módulo de bundle por domínio com o conjunto de arquivos
hexagonal.
Onde os portões aplicam tudo isso
- O portão de qualidade de exportação (Requisito 126, Contrato 3): com
view.exportBlockCriticalverdadeiro (o padrão), todo exportador se recusa a executar enquantomodelValidation.jsreportar qualquer problema de severidadeerror— o que inclui a regra de colisão de nomes OAS e papéis RBAC não aplicáveis (ver abaixo). A metade DOM do portão écanExportModelemapps/service-management/script.js. - As suítes de unidade são a aplicação. Todas as suítes nomeadas acima
rodam sob Bun pelo executor mapeado
(
ci-cd/run-unit-tests.js, que recusa uma execução que descobre zero testes) dentro debun run test:unit— uma célula de todo portão de qualidade de branch (ci-cd/run-branch-quality-gate.jsa seleciona para pushes diretos emdev, e a matriz completa para pull requests emdevemain). - Os portões do spec canônico permanecem verdes no lado do boilerplate:
bun run oas:check-routes(Req 036 sobrespec/) ebun run arch:check-boundariessão células doci:gate, de modo que um contrato canônico que o designer mira não pode silenciosamente deixar de ser aquilo contra o qual a exportação foi moldada.
Alinhamento RBAC e a divergência que ele reconciliou (JUM-477)
O editor RBAC por entidade está alinhado ao
Contrato de autorização de tenant e RBAC
através de
src/model/rbacContract.js,
um espelho no lado do designer da implementação do domínio Users (Rbac.ts,
TenantAuthorizationPolicy.ts). A reconciliação encontrou uma divergência
real, registrada aqui em vez de corrigida silenciosamente:
- O designer persistia
tenantScopedcomo um flag livre por regra; o runtime não tem esse controle. O escopo de tenant no runtime é derivado do conjunto de papéis (shouldRequireOrganization: papéis normalizadosadmin/userrestringem o principal à sua organização;superadmine escopos diretos legados mantêm uma fronteira global). Um valortenantScopedarmazenado que contradissesse os papéis era uma configuração que o boilerplate silenciosamente não honraria. O editor agora derivatenantScopeddos papéis selecionados (a caixa de seleção é somente leitura e pré-visualiza o valor derivado), e as políticas armazenadas são reparadas para o valor derivado no carregamento — uma extensão compatível sob o Requisito 126 Contrato 2, com o formato armazenado inalterado. - O vocabulário de principais é fechado. Apenas os papéis de tenant
normalizados (
superadmin,admin,user) e os escopos diretos legados são aplicáveis; qualquer outra coisa é rejeitada em tempo de edição e reportada comoerrorpela validação de modelo, de modo que o portão de exportação a bloqueia em vez de exportar uma política que o runtime descartaria.
Comprovado por:
rbacContract.test.ts,
que fixa o espelho contra o próprio Rbac.ts — se o vocabulário do domínio
divergir, a suíte reprova. É também por isso que o x-rbac faz ida e volta
sem perdas (Garantia 4): a política exportada é a normalizada e aplicável, e
o importador a reconstrói contra o mesmo contrato.
Exportação de suíte completa e a decisão sobre runtimeEnvironment (JUM-547, entregue)
Exportação e importação agora carregam as cinco abas, não apenas o
modelo de domínio. A exportação JSON (domain-designer.json) é o documento
versionado de suíte completa: { kind: "service-management-suite", version: "2.0.0", domains, relationships, interfaces, serviceConfiguration, runtimeEnvironment, codeWorkspace, deployments, view } — as mesmas seções que o documento
fixado service-management.v1 persiste no Cana (Requisito 126, Contrato 2),
menos as seleções de sessão e o idCounter. Um modelo desenhado nas cinco
abas exporta e reimporta com todas as abas intactas; um bundle exportado
antes desta mudança (o formato só de domínio, sem kind/version) importa
normalmente com as seções ausentes preenchidas com padrões, e um bundle com
seção desconhecida ou versão major mais recente falha claramente em vez de
ter sucesso parcial.
A decisão registrada é o tratamento de runtimeEnvironment
(JUM-547 ,
Requisito 126 Contrato 3): ele espelha conteúdo real de .env, então um
bundle de exportação contendo os valores seria um arquivo que pode carregar
configuração para fora da máquina. Das três posições candidatas — exportar
apenas a seleção; exportar valores restritos à camada editável; omitir a
seção inteiramente — a postura entregue é a primeira: o bundle carrega a
seleção de ambiente (environment, fileName), mas nunca os values, e a
importação restaura a seleção preservando os valores da máquina local. O
ambiente de runtime é uma propriedade de onde o designer está rodando; a
seleção é metadado de design que vale compartilhar. Como nenhum valor
atravessa, nenhum segredo pode sair em um bundle — a garantia pela qual a
terceira posição era preferida, mantida sem perder a seleção. A suíte prova
isso assertando que o documento no fio não contém nenhuma string de valor.
Comprovado por:
designerRoundTrip.test.ts
(deep-equal de suíte completa, valores-nunca-atravessam, compatibilidade
retroativa/para frente) e
designerExporters.test.ts
(formato do documento).
Referências
- Exportador/importador OAS:
packages/designer-core/src/exporters/designerExporters.js,designerImporters.js - Exportadores AsyncAPI/proto:
packages/designer-core/src/exporters/asyncApiExporters.js; validador:asyncApi30Validation.js - Codegen:
packages/designer-core/src/codegen/hexagonalCodegen.js - Validação de modelo / portão de exportação:
modelValidation.js,apps/service-management/script.js - Espelho RBAC:
src/model/rbacContract.js; contrato: Contrato de autorização de tenant e RBAC - Suítes:
designerRoundTrip.test.ts,designerPackageVersioning.test.ts,designerOasCompliance.test.ts,designerAsyncApiExport.test.ts,hexagonalCodegen.test.ts,rbacContract.test.ts,modelValidation.test.ts - Portões:
ci-cd/check-oas-route-resolution.js,ci-cd/check-hexagonal-boundaries.js,ci-cd/run-unit-tests.js - Alvos canônicos:
spec/1.0.0.yml,spec/asyncapi/,spec/asyncapi/1.0.0.grpc.yml,spec/asyncapi/async-api.proto - Requisitos: Requisito 036 (objetos de porta), Requisito 026 (conformidade de entidades OAS 3.1), Requisito 126, Contrato 3 (ownership e contratos públicos, Contratos 2–3)
- Documentos irmãos da cadeia E: Aplicação Service Management, Arquitetura de módulos do Service Management e contrato da porta IDesignerStore, Funcionalidades e uso do Domain Designer
- Linear: JUM-474 , JUM-475 , JUM-476 , JUM-477 , JUM-478 , JUM-470 , JUM-471 , JUM-547 , JUM-492