EZ3D — avaliação de arquitetura
Medido em 01/10/2026 pela régua da casa (REGUA.md). Repositório:~/ved/ez3d· HEAD5e9f618(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ério | Nota | Evidência | ||
|---|---|---|---|---|---|
| 1 | Metáfora central | 3 | Operante, 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 | ||
| 2 | Fisicalidade da metáfora | 3 | Medidor: 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) | ||
| 3 | Alcance na estrutura | 2 | Degrau 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 | ||
| 4 | Linguagem ubíqua | 1 | Dos 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 | ||
| 5 | Modularidade | 2 | A 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 | ||
| 6 | Isolamento | 2 | Adapter 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 | |
| 7 | Coesão | 2 | worker.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 | ||
| 8 | Objetos a serviço da metáfora | 2 | 7 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 | ||
| 9 | Desenhabilidade | 2 | Desenhou 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 | ||
| 10 | Contradições internas | 2 | Uma 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:
- Nomes de arquivo: 4 —
src/providers/{fal,types,tripo}.jseweb/src/ds/theme/ThemeProvider.tsx. Os três primeiros são uma pasta só (o conceito "fornecedor de geração 3D"); o quarto é convenção obrigatória do React. Dependência de terceiro: 0 (o vendordid-ianão tem nenhum arquivo com esses sufixos). - Identificadores: 11 — 6 são
*Providerde React Context (ThemeProvider,AuthProvider,ToastProvider,FilePickerProvider,StyleguideProvider,AppStatusProvider), 2 derivam da APIServiceWorker.controllerdo navegador (hadController,onControllerChange), 1 éassertVersioningHelpernum script de teste. Escolhas de abstração genuinamente livres: 2 (getProvider,defaultHandlerRef). Zero vendor.
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.
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.
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 deweb/src/é dividido por tela, o que é bom — mas é gaveta, ecreateObjectFlow.tsse declara "Orquestra o fluxo", que é nome de agente.
- 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.jssã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:modelstem 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'empedidoLeves.js:128,AND descricao_source <> 'user'emworker.js:638) em vez de uma peça que detenha a tabela.
- 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 (comparepublicarNoAcervo:777-809comconstruirEscadas:1071-1106edescreverModelos: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ção | Horas | Critério que move | De → para |
|---|---|---|---|
1. Trocar 'proprietaria' por 'propria' em src/acervoLod.js:203,228 e republicar as coleções existentes | 0,5 h | 10 — Contradições | 2 → 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 find | 1 h | 1 e 10 | reforç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 adapter | 1 h | 10 — Contradições | reforç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 acervo | 3 h | 6 e 7 | 6: 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 divergirem | 4 h | 6 e 10 | reforç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 h | 4 — Linguagem ubíqua | 1 → 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
- Comportamento em produção. Li o código, o schema e as provas; não rodei o app nem abri
ez3d.did.lu. As afirmações sobre o que acontece em produção (a rota que responde 404, a licença divergente nas fichas já publicadas) são deduzidas do código, não observadas no ar. As duas são verificáveis em minutos por quem tiver acesso. - As provas não foram executadas. Li as 8 baterias (3.338 linhas) e julguei o que cobrem pelo código delas. Não rodei nenhuma —
test:migrationexige Docker,test:descricaogasta uma chamada real de IA, e o tópico é de leitura. - O lado do acervo foi lido, não medido.
~/ved/asset-pipeline-dashboardé pasta compartilhada e havia outra sessão de pé nesta máquina. Li o código para apurar os dois lados de cada contradição da fronteira, que é o que a régua exige, mas não medi nada a partir de lá e não avaliei a arquitetura dele — isso é outra varredura. - O
videomocapnão foi avaliado. A pergunta sobre a jornada foi respondida pelo lado do EZ3D (ele não conhece o vizinho, e está certo). Se a costura da jornada tem dono do outro lado, isso aparece na varredura do videomocap, não nesta. - A fisicalidade do front recebeu menos atenção que a do backend. São 10.300 linhas e 92 arquivos; li a estrutura, os nomes, a gaveta e o
motor.ts, não os 92 arquivos. - Uma divergência do contraditório ficou sem desempate, e é a mais interessante. O avaliador adversarial sustenta que o EZ3D tem duas metáforas e nenhuma manda — a oficina (setembro/outubro) e a máquina de estados herdada do
almost-scan(junho), queARCHITECTURE.md:6eDECISIONS.md:6declaram como o padrão do projeto. Ele põe metáfora central em 2 por isso; eu mantenho 3. Quem decidir essa diferença decide se oworker.jsde 1.363 linhas é uma peça grande demais ou o lugar onde duas arquiteturas se encontraram sem se reconciliar — e isso muda a ação de "quebrar o worker em quatro" para "escolher qual metáfora governa antes de quebrar". - Uma alegação minha caiu no contraditório e não foi substituída. Escrevi que havia cinco cópias de regras do acervo dentro do EZ3D; sob verificação, sustento duas com os dois lados (o limite de 300 do campo
usoemacervoLod.js:128e o regex de slug emtest-slug-acervo.js:27, este já divergente da fonte emasset-pipeline-dashboard/src/validate.js:5). As outras três eu não consegui provar, e saem.