← Arquitetura da Casa

Avaliação · ez3d

EZ3D — a oficina onde um objeto do mundo vira arquivo

Entra foto por uma de duas portas, passa por quatro estações de uma esteira, sai peça na estante da casa. A metáfora é forte, é física, e nunca foi declarada — e o que ela não tem é um nome por coisa.

01 de outubro de 2026  ·  medido pela régua da casa  ·  HEAD 5e9f618 (0.7.1)  ·  notas submetidas a contraditório adversarial

19/30soma pela régua
4 / 11medidor de cinco segundos
(arquivos / identificadores)
Bcamada que puxou para baixo
(o alcance)
3defeitos de produção achados na leitura
Desenho da arquitetura do ez3d.
O desenho da arquitetura. Caixa ciano é peça com equivalente no mundo; caixa cinza tracejada é peça sem corpo físico — e cada uma delas é um achado.

EZ3D — avaliação de arquitetura

Medido em 01/10/2026 pela régua da casa (REGUA.md). Repositório: ~/ved/ez3d · HEAD 5e9f618 (0.7.1) · árvore limpa. Backend 9.100 linhas em 30 módulos · front 10.300 em 92 arquivos · 12 migrações · 8 baterias de prova (3.338 linhas).

1. A metáfora em uma frase

EZ3D é a oficina onde um objeto do mundo vira arquivo. Entra foto por uma de duas portas, passa por quatro estações de uma esteira, e sai peça na estante da casa — com uma escada de versões leves ao lado, para quem vai montar cena.

Ela é operante e nunca foi declarada. Não há documento que diga "a metáfora é a oficina"; não há docs/metafora.md, e o README abre com stack ("PWA mobile-first: fotografe um objeto, gere um modelo 3D"). Mas o vocabulário físico está em toda parte e é consistente: 1.948 ocorrências de palavras com corpo — a porta de máquina (server.js:81), a bancada de cena (server.js:86), a escada de degraus (src/lod.js:71, ESCADA), o mestre (src/acervoLod.js:3), a casca interna (src/lod.js:317), a ficha no acervo, a boca do bonequinho (scripts/print-boca.mjs).

A frase mais próxima de uma declaração é um comentário de arquitetura, e é boa: docs/ARCHITECTURE.md:100 — "o EZ3D não monta nada". Ela define a obra por negação, que é a metade difícil; falta a metade fácil, a afirmativa. Escrever a frase é a ação de uma hora mais barata deste relatório.

Onde a metáfora não chegou é igualmente nítido, e é no lugar mais visível: as pastas de primeiro nível não contam história nenhuma — src/, web/, docs/, scripts/, migrations/, vendor/, fixtures/. Essa lista é a mesma em qualquer projeto Node do mundo. Um agente que cai aqui não ganha intuição da estrutura; ele a ganha abrindo os arquivos, onde a oficina está.


2. Notas

#CritérioNotaEvidência
1Metáfora central3Operante, não declarada. server.js:81,86 (porta, bancada); src/lod.js:71 (ESCADA); 1.948 ocorrências de vocabulário físico. Desconto: pastas de 1º nível são categorias técnicas
2Fisicalidade da metáfora3Medidor: 4 arquivos / 11 identificadores, dos quais 6 são *Provider de React Context e 2 derivam de ServiceWorker.controller. Zero Manager/Service/Handler/Util/Helper/Factory/Processor. Chega ao nome de função: gerarEscada (lod.js:716), gerarDegrau (:422), adicionarCascaInterna (:317), publicarEscada (acervoLod.js:170), construirEscadas (worker.js:1058), glbMestre (worker.js:1161)
3Alcance na estrutura2Degrau 4 alcançado; degrau 5 partido ao meio. 8 de 8 tabelas em inglês (002_schema.sql:68-241); as 44 colunas de junho 100% inglês genérico; as de setembro/outubro português (acervo_projeto, lod_colecao_slug, lod_versoes, leves_pedidos, descricao_medidas). Borda HTTP: 21 rotas inglês, 12 português, 3 híbridas
4Linguagem ubíqua1Dos 9 conceitos centrais, zero tem nome único. "Versões leves" tem 6 nomes vivos ao mesmo tempo: leves_pedidos (010:96), lod_versoes (006:51), lod_presets (010:28), rota /leves (objects.js:343), degrau/escada no código (lod.js:20,71), "Versões leves" na tela (VersoesLeves.tsx:59). Nenhum gate de nome em lugar nenhum
5Modularidade2A troca de provider aconteceu de verdade (23281ab, 23/06): fora de src/providers/ mudaram types.js, generations.js (5 linhas) e worker.js (+49, e foi humanizeError). Mas a régua pede contorno em cada peça, e as outras duas pesadas reprovam: src/lod.js não tem contrato declarado e é chamado por função solta de worker.js:1058 e routes/admin.js:128; src/acervo.js:312 exporta slugParaAcervo — conversão de namespace interno — através da fronteira. E há núcleo compartilhado, que é a definição de nota 2: 18 de 30 módulos escrevem SQL contra as mesmas tabelas
6Isolamento2Adapter e vendor exemplares. Mas: 18 de 30 módulos escrevem SQL inline (db.js são 46 linhas de pool); models tem 4 escritores na mesma máquina de estados (worker.js:1073, pedidoLeves.js:120, admin.js:51, maquina.js:876); o contrato real worker↔provider inclui o texto do erro (worker.js:1326 faz s.includes('saldo'), :1341 ramifica em `s.includes('fal')s.includes('tripo') — no mesmo repositório cujo providers/types.js:2 declara que o worker nunca conhece provider concreto); o nome do provider vaza para 6 módulos fora de providers/ (image.js:9,23,30, dedupe.js:11,24, descricao.js:120`). Zero teste de fronteira
7Coesão2worker.js 1.363 linhas = 4 filas independentes repetindo a mesma estrutura 4 vezes, com 2 timers. routes/maquina.js 1.134 linhas = 6 assuntos, e /gera sozinho tem 415. web/src/lib/ é gaveta de 9 arquivos. lod.js faz decimação e casca. sharePage.js injeta OG e conta views. makeSlug duplicado (objects.js:39, maquina.js:96). Contrapeso: 15 módulos passam no teste da frase, 6 são folhas puras com injeção de dependência
8Objetos a serviço da metáfora27 classes no projeto: 6 são tipos de erro; uma só é uma coisa — MotorCena (web/src/cena/motor.ts:143), com estado, e com nome físico. As peças centrais da metáfora (escada, degrau, mestre, casca) são funções e strings literais inline. Mas não há agente falso nenhum, e isso é o que a régua pune
9Desenhabilidade2Desenhou e fechou na maior parte. 4 caixas cinzas (§4), 1 ligação tracejada. As 4 estações da esteira existem como conceito e não como peça no disco
10Contradições internas2Uma interna confirmada e barata (o mapa do repositório descreve 9 caminhos que não existem, ARCHITECTURE.md:216-230); uma declarada com razão escrita e condição de saída (DECISIONS.md:32 reconhece que o provider default é o fal); duas são com o vizinho e não internas — a licença oposta (acervoLod.js:203,228) e a fronteira do osso sem gate. A régua dá 2 a "pontuais e periféricas, ou declaradas com razão e precedência escritas", e o EZ3D satisfaz as duas metades

Soma: 19/30.

Os dois números do medidor de cinco segundos:

Pela calibração da régua, esses dois números põem o EZ3D no mesmo degrau do asset-pipeline-dashboard e do video-mocap — e muito longe do clag.

Estas notas passaram por contraditório. Submeti as dez a um avaliador adversarial com a instrução de derrubá-las, e ele mudou quatro. Aceitei três: modularidade caiu de 3 para 2 (a troca de provider foi uma prova só, e lod.js e acervo.js reprovam no mesmo teste); contradições subiu de 1 para 2 (duas das quatro ou estão declaradas com razão escrita, ou são com o vizinho e não internas); e a reversão do osso saiu da lista de contradições pela cronologia. Rejeitei a quarta — ele queria metáfora central em 2, por haver duas metáforas concorrentes (a oficina e a máquina de estados herdada do almost-scan). O argumento é bom e está registrado no parágrafo acima; mantenho 3 porque a máquina de estados não é uma metáfora rival, é o mecanismo que a esteira usa — queued → downloading descreve o que acontece com os bytes, não uma segunda imagem do mundo.

A camada que puxou o resultado para baixo é a B — o alcance (3 e 4, somando 3 de 6). A metáfora é forte e a forma é boa; o que falha é a metáfora não ter um nome por coisa. É o diagnóstico mais acionável do relatório, porque não exige refatorar nada: exige escolher.


3. O desenho

relatorios/ez3d.svg — publicado junto com este relatório.

Cada caixa é um objeto do mundo; cada seta é uma ligação que existe no código, rotulada com o que passa por ela. As quatro caixas cinzas tracejadas são as peças sem corpo físico, e estão na seção 4. A única ligação laranja tracejada é a que o desenho precisou e o código não tem como parte da obra.


4. Onde o desenho não fechou

Peça sem corpo — quatro.

  1. src/providers/ — a caixa que o contrato salvou e o nome não. O contrato é dos melhores que li na casa (providers/types.js:26-32): três métodos, vocabulário canônico normalizado, e o worker que nunca sabe quem está do outro lado. O nome, porém, não é coisa nenhuma — "fornecedor" descreve quem age. Numa oficina existe fornecedor; na metáfora não existe peça com esse nome. É 1 dos 3 arquivos do medidor de abstração, e o único que é escolha real de domínio.
  1. web/src/lib/ — a gaveta. Nove arquivos sem assunto comum: cliente HTTP (api.ts), login (auth.ts), redimensionar foto (resize.ts), fila de upload (uploadStore.ts), conectividade (useOnline.ts), aviso de versão nova (usePwaUpdate.ts), fixtures de teste (fixtures.ts), tipo (objects.ts) e hook de lista (useObjects.ts). É pequena e localizada — o resto de web/src/ é dividido por tela, o que é bom — mas é gaveta, e createObjectFlow.ts se declara "Orquestra o fluxo", que é nome de agente.
  1. O acesso ao caderno — peça invisível. O desenho precisou de uma peça que fosse "o banco" e o código não tem: db.js são 46 linhas de pool e transação, e 18 dos 30 módulos escrevem SQL inline, 44 chamadas só no worker. A consequência não é teórica: models tem 4 escritores mexendo nas mesmas colunas de estado, e a proteção contra corrida é uma convenção repetida em cada sítio (AND lod_status <> 'building' em pedidoLeves.js:128, AND descricao_source <> 'user' em worker.js:638) em vez de uma peça que detenha a tabela.
  1. As quatro estações da esteira — conceito sem arquivo. O tick (worker.js:134-148) é uma esteira legível: zelador, reconstrução, depósito, descrição, escada. Mas nenhuma estação é uma peça no disco — não há worker/reconstrucao.js, worker/deposito.js. São 1.363 linhas e quatro assuntos num arquivo, e os quatro repetem a mesma estrutura de claim/backoff/sweeper (compare publicarNoAcervo:777-809 com construirEscadas:1071-1106 e descreverModelos:493-529: é o mesmo SQL com os nomes de coluna trocados, três vezes).

Ligação que não existe no mundo — uma. scripts/regera-cabeca.js monta corpo humano direto na estante, pela porta de admin. Na metáfora da oficina, a oficina não entra na estante para remontar o que já está guardado. Detalhe em §5.

Peça invisível — uma, além do caderno. A jornada que o founder quer validar (foto → peça → humano animado) não existe em lugar nenhum do código dos dois lados. Detalhe em §5.


5. Contradições

Internas

(a) O mapa do repositório descreve uma casa que não existe mais. Tipo 3. docs/ARCHITECTURE.md:216-230 (§8, "Estrutura") lista src/glb.js, src/providers/meshy.js, src/providers/index.js, web/App.tsx, web/api/client.ts, web/components/, web/pages/, vite.config.ts, index.html. Conferi os nove: nenhum existe. A estrutura real divide web/src/ por tela (home/, object/, share/, cena/, settings/, shell/) com o design system em ds/ — um desenho melhor que o descrito, aliás. Quem deveria ganhar: o disco. A seção §8 deve ser regerada do find, não editada à mão.

(b) Os documentos dizem Tripo; produção roda fal — mas o projeto sabe disso. Tipo 3, atenuada. README.md:3,130 e docs/DECISIONS.md:5 ("Provider de referência: Tripo") × did.json:18-22: DEFAULT_PROVIDER=fal, FAL_STUB=false, TRIPO_STUB=true — o Tripo nem roda. Quem reconstrói em produção é Trellis pelo fal desde 23/06 (23281ab). Atenuante decisivo, que eu tinha perdido na primeira leitura: o mesmo arquivo declara a situação 27 linhas abaixo. docs/DECISIONS.md:32 — "FAL_KEY fica required: false, sob protesto registrado. Ela é o provider default (DEFAULT_PROVIDER=fal, FAL_STUB=false)" — com a razão de ainda ser assim, o registro de que tentou mudar e desfez, e a condição de saída escrita. Pela régua, isso é exatamente o que separa nota 1 de nota 2. O que resta é a linha 5 e o README não terem sido varridos junto. O agravante vira elogio: a contradição é prova de que o adapter funciona. A troca passou tão limpa que a documentação de cima não percebeu. Quem deveria ganhar: o did.json e a linha 32. E MESHY_API_KEY continua declarado (did.json:12) para um provider que é uma linha comentada (types.js:40).

(c) As versões leves entram no acervo com a licença oposta à do mestre. Tipo 5 (vocabulário divergente entre vizinhos) — a mais cara, e é um bug ativo. Classifiquei como interna na primeira passada e estava errado: o EZ3D não manda licença nenhuma no mestre, e diz isso em src/acervo.js:13 ("O que o EZ3D NAO manda: licenca, origem, autor […] Quem afirma isso e o acervo, nao quem deposita"). A divergência é entre o que o acervo carimba e o que o EZ3D declara pela outra porta — o que torna o defeito mais nítido, não menos. O mestre nasce licenca: 'propria' — "Própria (feita pela casa)" — e quem escreve é o acervo, server-side (asset-pipeline-dashboard/src/ferramentas.js:79), justamente para o gerador não poder mentir sobre procedência. Os degraus nascem licenca: 'proprietaria' — "Proprietária (comprada/licenciada)" — declarado pelo EZ3D (src/acervoLod.js:203,228), porque o caminho LOD não passa pela porta de ferramentas. Os dois valores existem no vocabulário (asset-pipeline-dashboard/src/catalog.js:99-100) com significados opostos, e parseCatalogFields só checa se o valor existe na tabela. Resultado: na coleção <slug>-versoes-leves, o mestre diz que a casa fez e as quatro versões leves do mesmo arquivo dizem que a casa comprou de terceiro. A ficha do acervo existe para responder, meses depois, se o material pode ser usado — e hoje ela responde duas coisas contrárias sobre os mesmos bytes. Quem deveria ganhar: 'propria', e o conserto estrutural é a porta de ferramentas crescer para saber de coleção, devolvendo os quatro campos ao server-side.

(d) 251 linhas de anatomia dentro do projeto que declarou não conhecer anatomia. Rebaixada de contradição para achado de coesão, depois do contraditório. scripts/regera-cabeca.js:3-8,102,110,154-156,221 sabe o que é esqueleto de 109 nós, três animações, o nó mixamorigHead, e que o prune come 53 ossos se não for segurado. Chamei isso de contradição com o revert do osso; a cronologia me desmente. O script entrou às 01:19 de 30/09 (b99a7b4) e o revert é das 22:25 do mesmo dia (0e595bd) — ele precede a decisão, não a ignora. E migrations/011_sem_osso.sql:16-19 escreve a precedência que o acomoda: "A afirmação 'esta malha vai no osso head' continua sendo verdadeira e continua sendo gravável — pelo acervo". O script fala exclusivamente com a API do acervo; está do lado certo da linha. O que resta, e continua valendo: 251 linhas sobre anatomia de boneco moram no repositório errado, e são o precedente que o próximo pedido parecido vai encontrar aqui dentro. É assunto de coesão e de higiene, não de contradição. Para onde deveria ir: o lado do acervo / did-asset.

Com os vizinhos

(e) A fronteira do osso está no código e não tem gate. Tipo 5 em potencial. A remoção foi cirúrgica e eu a conferi inteira: zero osso/categoria/catalogação no runtime — rotas, worker, acervo, banco, provas, build e vendor. O depósito manda cinco campos e nenhuma categoria (src/acervo.js:144-160). A descrição herdou a fronteira como instrução ao modelo (src/descricao.js:113: "Não diga para que o objeto serve, onde ele se encaixa nem a que conjunto pertence"). Mas nada recusa o vocabulário voltando. A porta de máquina ignora campo desconhecido em silêncio; não há constraint, não há prova de fronteira. O único detector que existiu foi o DROP TYPE da própria migração (011:41), que já rodou. Pela régua, é a diferença entre 2 e 3: lei sem gate é intenção. Do outro lado, o acervo prevê a volta: asset-pipeline-dashboard/src/peca-de-humano.js:28 tem um bilhete dizendo que "o EZ3D vai depositar peça com o osso declarado nos bytes". Dois repositórios com planos opostos sobre a mesma fronteira, nenhum dos dois sabendo do outro. Quem deveria ganhar: a decisão do EZ3D (é mais recente e melhor argumentada). O bilhete do acervo deveria ser atualizado ou removido.

(f) O caminho LOD atravessa sem contrato, e já há uma cópia divergente. Tipo 2. O mestre atravessa pela porta de ferramentas — contrato de verdade: o EZ3D declara nome, endereço e bytes, e licença/origem/autor/uso/categoria vêm do registro server-side do acervo (ferramentas.js:74-93). Há teste dos dois lados, e o do acervo é bom: ele exercita o cliente tentando mentir e a ficha ignorando (test/integracao-ez3d-sync.js:180-207). Os degraus atravessam pela API de catálogo crua, onde o EZ3D declara tudo. Isso carregou para dentro do EZ3D pedaços do vocabulário interno do acervo, nenhum sob contrato — e um já divergiu: o regex de slug copiado em scripts/test-slug-acervo.js:27 (/^[a-z0-9][a-z0-9-]$/) é mais estrito que a fonte em asset-pipeline-dashboard/src/validate.js:5 (/^[a-z0-9-]+$/). Benigno hoje; é exatamente o modo de falha que o próprio acervo documentou ao criar uma sentinela para o clag (scripts/confere-contratos.js:8-11: "cópia é segunda fonte, e segunda fonte envelhece calada"*). O EZ3D tem cópias caladas (o limite de 300 do campo uso em acervoLod.js:128, o regex acima) e nenhuma sentinela. Quem deveria ganhar: o acervo, que é o dono do vocabulário. A saída é a porta de ferramentas saber de coleção, ou uma sentinela no EZ3D.

(g) A jornada que o founder quer validar não existe no código de nenhum dos lados. Tipo 2, por ausência. O pedido fala em "ez3d + videomocap = humanos animados com peças em seus ossos". Procurei: o EZ3D não menciona o videomocap em lugar nenhum — nem em código, nem em doc, nem em comentário. E está certo que não mencione, pela fronteira de (e). A costura acontece no acervo, em três gestos manuais e separados: o EZ3D deposita como modelo3d; alguém roda did-asset peca para declarar o osso; alguém monta. O regera-cabeca.js é a prova de que esse terceiro gesto hoje é um script avulso. O achado não é que falte código de integração — a fronteira está certa e deve ficar. É que a jornada não tem dono nem documento: ela existe na cabeça de quem a executou e em nenhum repositório. Isso é a "peça invisível" do critério 9, em escala de família.


6. Ações propostas

Ordenadas por nota que movem por hora gasta. Horas são de trabalho de agente.

AçãoHorasCritério que moveDe → para
1. Trocar 'proprietaria' por 'propria' em src/acervoLod.js:203,228 e republicar as coleções existentes0,5 h10 — Contradições2 → 3
2. Consertar os dois defeitos achados na leitura: req.ownerId → req.dbUser.id em maquina.js:850 (a rota responde 404 sempre), e chamar recoverDescricaoOrphans no tick (worker.js:134)1 h— (não move critério; entra porque é defeito de produção)—
3. Escrever a frase da metáfora no topo do README e regerar ARCHITECTURE.md §8 a partir do find1 h1 e 10reforça 1 e 10
4. Atualizar README.md:3,130 e DECISIONS.md:5 para dizer o que a linha 32 já diz — que o provider é o fal — e tirar MESHY_API_KEY ou implementar o adapter1 h10 — Contradiçõesreforça 3
5. Um gate de fronteira que recuse o vocabulário de montagem — uma prova que varra src/ e migrations/ procurando osso/peça-de-humano e reprove; e mover scripts/regera-cabeca.js para o lado do acervo3 h6 e 76: 2 → 3
6. Uma sentinela de contrato com o acervo, no molde do confere-contratos.js que o próprio acervo escreveu: confere USO_MAX, o regex de slug e os vocabulários kind/categoria/licenca contra a fonte, e quebra quando divergirem4 h6 e 10reforça 3
7. Escolher UM nome para a escada e levá-lo a banco, rota, módulo, código e tela — hoje são seis, e o mesmo arquivo usa duas formas em duas linhas (maquina.js:314 leves, :394 levesPedidas, contra a coluna leves_pedidos e o módulo pedidoLeves.js). Com um teste que reprove as outras formas em src/6 h4 — Linguagem ubíqua1 → 2

As ações 1 e 2 deveriam sair hoje: a primeira é uma palavra que hoje faz a ficha do acervo mentir sobre procedência; a segunda são dois defeitos de produção, um deles uma rota inteira morta.

A ação 7 é a mais cara e a que mais move a camada que puxou a nota para baixo. Vale notar que ela não exige refatorar nada — exige escolher um nome e varrer. E a ordem recomendada é fazer a 5 e a 6 antes dela: o padrão da casa, medido na própria régua, é que o gate pega e a lei não.


7. O que eu não consegui avaliar