Arquitetura
O que existe no código, e por quê. O porquê de cada escolha está em DECISOES.md; aqui está a
forma que elas tomaram.
O princípio
Seção intitulada “O princípio”O grafo é a única fonte de verdade. Fórmula, descritores, coordenadas 3D, nota de missão e texto do tutor são derivados dele e recalculáveis a qualquer momento. Nada além do grafo — e do rastro do que a pessoa fez com ele — é persistido como estado do usuário.
Isso mantém a persistência mínima, torna qualquer defeito reproduzível a partir de um único objeto e permite trocar a implementação de qualquer camada derivada sem migração de dados.
Fluxo, do traço ao veredito
Seção intitulada “Fluxo, do traço ao veredito” ┌──────────────────────────────┐ │ RDKit · WASM (worker) │──► métricas, grupos, estereo, ┌── molblock ─► valência, descritores │ veredito da missão │ └──────────────────────────────┘ │ lê Editor 2D ──► GRAFO ▼ (canvas (fonte da ┌───────────┐ próprio) verdade) ┌──────────────────────────────┐ │ Tutor LLM │ │ │ OpenChemLib · WASM (worker) │ └───────────┘ └── grafo ────► conformação + MMFF94 │──► cena 3D, dinâmica, ▲ │ Hessiana → modos normais │ modos normais │ lê └──────────────────────────────┘ └──────────────┘
◄────────────── nunca escreve ──────────────────────────────┘Cada átomo que sai da geometria carrega o índice do átomo do grafo que o originou (source), e o
hidrogênio acrescentado pelo campo de força carrega o do vizinho em que está pendurado. É por
esse índice que o vértice do desenho e a esfera da cena acendem juntos, nos dois sentidos (D-18).
O caminho de volta está desenhado justamente porque não existe. O tutor lê as métricas e o veredito e não tem como alterar o grafo, os descritores ou a nota. É a garantia estrutural de que nenhuma afirmação química chega ao usuário sem ter passado pelo motor determinístico.
Pacotes
Seção intitulada “Pacotes”apps/web Next.js 16 App Router — rotas, ações de servidor, contas, turmas, listas, tutorpackages/ core grafo · ponte com o RDKit (worker e Node) · geometria, dinâmica e modos normais editor2d canvas 2D próprio: ferramentas, seleção, menu de contexto, atalhos, histórico viewer3d Three.js + React Three Fiber: dobramento, vibração, modos — render, nenhuma química quests missões declarativas, extração de objetivos e pontuação ui tokens, CPK e componentes básicosA regra de dependência: core não depende de ninguém, e ninguém depende de editor2d.
corenão conhece React, Three.js nem o DOM. Roda em teste de linha de comando, o que torna a química testável sem navegador e rápida de verificar. O RDKit entra por dois carregadores — o do navegador (chemistry/browser.ts, dentro do worker) e o do Node (chemistry/node.ts, do pacote npm) — e é o mesmo motor nos dois.questsdepende só decore: uma missão é uma lista de condições sobre a molécula que o RDKit analisou, nunca sobre o desenho. Por isso a mesma função avalia no navegador, para resposta instantânea, e no servidor, antes de gravar.- Nenhum pacote depende de
editor2d— ele é folha. Quem o importa é o app, que precisa dele para desenhar a tela; o que a regra garante é que a interface de desenho seja substituível sem tocar em nada abaixo dela.
Quando uma dessas regras precisar ser quebrada, a resposta certa quase sempre é mover a lógica
para core, não criar a dependência.
O que mora em cada um
Seção intitulada “O que mora em cada um”| Pacote | Dentro |
|---|---|
core/graph |
tipos do grafo, operações (átomo, ligação, cunha, carga), molblock V2000 |
core/chemistry |
protocolo do worker via Comlink, analyze (sanitização, descritores, grupos por SMARTS, estereo R/S/E/Z), depict (SVG do RDKit), tidy (organizar o desenho), elementos |
core/geometry |
conformação e MMFF94 pelo OpenChemLib, velocity-Verlet a 300 K, Hessiana numérica e diagonalização de Jacobi para os modos normais |
editor2d |
store.ts (Zustand, histórico, seleção), render.ts, Toolbar, PeriodicTable, ContextMenu, Shortcuts, Popover, keys.ts, templates.ts (anéis) |
viewer3d |
Viewer3D, Molecule (esferas e varetas, CPK), folding.ts, sticks.ts |
quests |
catalog.ts (15 missões), conditions.ts, evaluate.ts, extract.ts (objetivos a partir da molécula, D-25), types.ts |
ui |
tokens.css, cpk.css, base.css, Button, Card, Formula, Label, Logo, NumberValue, SourceBadge |
Stack e o porquê
Seção intitulada “Stack e o porquê”| Camada | Escolha | Por quê |
|---|---|---|
| Aplicação | Next.js 16 (App Router) + TypeScript estrito | SSR nas páginas públicas de molécula; ações de servidor sem camada de API à parte |
| Química | RDKit.js (WASM) em Web Worker via Comlink — e no servidor, do pacote npm | Ver D-02. O mesmo motor reavalia no servidor o que o navegador avaliou |
| Geometria | OpenChemLib: conformação + MMFF94 | O RDKit.js publicado não traz gerador 3D nem campo de força. Ver D-10 |
| Vibração | Velocity-Verlet sobre o gradiente numérico do MMFF94, a 300 K | Mesma física do dobramento, outro regime. Ver D-14 |
| Modos normais | Hessiana por diferenças finitas, ponderação por massa, projeção do corpo rígido, Jacobi | 3N − 6, ou 3N − 5 quando linear. Ver D-20 |
| 3D | Three.js + React Three Fiber | Motor de render apenas — nenhuma química dentro |
| Editor 2D | Canvas 2D próprio + Zustand | É o diferencial; nenhuma lib pronta dá o toque certo |
| Dados | Postgres + Prisma 7 com o driver pg |
Container em desenvolvimento e na imagem Docker; sem serviço gerenciado. Ver D-11 |
| Contas | bcrypt (custo 12); sessão por token aleatório, com só o resumo SHA-256 no banco | Cookie httpOnly, sameSite=lax, secure em produção; 30 dias |
| LLM | Gemini, rota de servidor, JSON de schema fechado | Schema fechado impede o modelo de inventar campo químico. Opcional: sem chave, desliga |
| Telemetria | Umami auto-hospedado, opt-in | Sem cookie; desligada por padrão; lista fechada de momentos em apps/web/lib/track.ts |
| Entrega | Imagem Docker (Dockerfile, saída standalone do Next) + docker-compose.yml com Postgres |
Self-host em três comandos (D-28). Ver INSTALACAO.md |
O servidor
Seção intitulada “O servidor”Toda escrita passa por uma ação de servidor em apps/web/app/actions/, com a entrada
validada por zod. Não existe rota de API pública além das páginas.
| Arquivo | O que faz |
|---|---|
account.ts |
criar conta, entrar, sair |
attempt.ts |
gravar tentativa (saveAttempt), abrir missão (openQuest), ler progresso |
library.ts |
guardar, tirar e listar moléculas — a estante |
naming.ts |
batizar uma estrutura (D-15), ler apelido e o estado do batismo |
search.ts |
busca por nome no PubChem, com cache no banco e disjuntor |
tutor.ts |
perguntar ao tutor, com cache por (inchiKey, missão, tipo) e teto diário |
recovery.ts |
emitir código de troca de senha (quem dá aula) e trocar a senha com ele (D-19) |
staff.ts |
ler quem dá aula na escola, promover a professor e rebaixar — só administrador (D-29) |
classroom.ts |
abrir turma, entrar com código, ler o quadro da turma (D-22) |
assignment.ts |
listas da turma, missão de professor, catálogo buscável e denúncia (D-25 a D-27) — ver ROTEIROS.md |
Regras que valem em toda ação:
- O navegador manda o desenho, nunca o veredito. A
specda missão roda no navegador para dar resposta instantânea, e roda de novo no servidor — sobre o molblock, reanalisado pelo RDKit do Node — antes de gravar qualquerAttempt. - Papel é pré-requisito; dono é a autorização.
requireTeacherdiz que a conta dá aula; cada escrita começa porownedClassroom/ownedAssignment/ownedTeacherQuest(lib/roles.ts). Ninguém se autodeclara: o papel vem do scriptapps/web/scripts/promote-teacher.mjs, rodado por quem administra a instância (D-19), ou de um administrador da própria escola, que promove só até professor (D-29). Os três papéis —aluno,professor,administrador— são lidos num lugar só,teachesemlib/roles.ts. - Recusa uniforme. Slug que não existe e slug a que a conta não tem acesso recebem a mesma
frase,
Essa missão não existe.; senha errada e e-mail inexistente recebem a mesmaE-mail ou senha não conferem.. Ação não vira oráculo de existência. - Sem banco, sem muro.
hasDatabase()é falso quandoDATABASE_URLnão existe: conta, turma, lista e estante somem da interface, e o editor funciona inteiro. - Tetos. Pedidos ao tutor por conta por dia (
TutorUsage); conferências de missão por conta por minuto, salvamentos de autoria por dia, códigos de turma errados por hora e consultas de papel por administrador por hora — estes em memória, um processo só.
Modelo de dados
Seção intitulada “Modelo de dados”O schema com os comentários de cada coluna é apps/web/prisma/schema.prisma; as migrações estão
ao lado, em prisma/migrations/, e são todas aditivas.
| Grupo | Tabelas | O que guardam |
|---|---|---|
| Conta | Profile, Session, ResetCode |
quem usa (role é aluno ou professor, institution é a escola), sessões por resumo do token, códigos de troca de senha por resumo |
| Trabalho | Molecule, MoleculeName, PubChemName, PubChemKnown |
a estante (o grafo, único por dono e InChIKey), o apelido por InChIKey com autoria, e o que o PubChem já respondeu |
| Missão | Attempt, QuestOpen, TutorHint, TutorUsage |
tentativas com a nota reavaliada no servidor, quem abriu o quê (é o “travou” do quadro, D-22), cache do tutor, contagem diária |
| Turma | Classroom, Enrollment, Assignment, AssignmentItem, TeacherQuest, QuestReport |
turma com código, matrícula, lista com itens ordenados, missão de professor com a resposta e os objetivos extraídos dela, denúncia |
Não existe tabela de missão do catálogo. O catálogo vive em packages/quests/src/catalog.ts,
versionado com o motor que o avalia (D-12): uma missão no banco poderia discordar da spec que
o código executa, e a nota mudaria sem ninguém ter mexido em nada. Attempt.questSlug guarda o
slug; missão de professor usa o mesmo campo, no espaço de nomes professor:<id>.
Gabarito não sai. TeacherQuest.answerMolblock e answerInchiKey não entram em nenhum
select de caminho de aluno (R-3, ROTEIROS.md).
Como o produto sobe
Seção intitulada “Como o produto sobe”- Desenvolvimento:
docker compose up -d postgressobe só o banco;pnpm devsobe o app comapps/web/.env. Opredevcopia o RDKit e as tabelas do MMFF94 denode_modulesparaapps/web/public/chem/e gera o cliente do Prisma. - Imagem: o
Dockerfileconstrói em três estágios e carrega só a saídastandalonedo Next, mais o schema, as migrações, o script de promoção e a linha de comando do Prisma. Odocker/entrypoint.shaplica as migrações pendentes quando háDATABASE_URL, e sobe só o editor quando não há. Nenhum segredo entra na imagem. - Instância completa:
docker compose up -dsobe app e Postgres, com o volumerotamer-postgresguardando os dados. Detalhes, variáveis e operação emINSTALACAO.md; a instância mantida pelo autor, emDEPLOY.md. - Integração contínua (
.github/workflows/ci.yml): um trabalho sem banco (lint, tipos, testes de núcleo — os de ação pulam avisando), um com Postgres (migração, testes de ação comREQUIRE_DATABASE=1, Playwright em desktop e celular), e um que constrói a imagem e a sobe contra um banco novo. A imagem é publicada no GHCR a cada etiquetav*(image.yml).
Desempenho — restrições, não sugestões
Seção intitulada “Desempenho — restrições, não sugestões”- Tudo pesado no worker. Sanitização, descritores, conformação, dinâmica e Hessiana nunca tocam a thread principal. O desenho continua a 60 fps enquanto o RDKit trabalha.
- Debounce por intenção, não por tempo fixo. Métricas a cada 120 ms de silêncio; geometria 3D apenas quando a topologia muda, nunca quando um átomo é arrastado.
- Cache por InChIKey. Conformação, trajetória e descritores são função pura do grafo. A mesma molécula nunca é calculada duas vezes.
- O WASM carrega depois da primeira pintura, e a cena 3D sob demanda.
/chem/*vai comimmutable, e oprebuildgrava.bre.gzao lado para um proxy que saiba servi-los. Meta medida: primeiro desenho interativo em menos de 3 s num celular fraco em 3G, e 605 ms na revisita com o motor em cache.
Escola pública em celular ruim é o caso de uso, não o caso extremo.
A camada de IA
Seção intitulada “A camada de IA”O tutor é o único trecho do produto que pode estar errado, e é o único marcado em âmbar.
- O prompt (
lib/tutor/prompt.ts) recebe os descritores já calculados, os grupos que o RDKit reconheceu e o veredito da missão objetivo a objetivo, e é instruído a nunca recalcular nem contradizer. - A saída (
lib/tutor/schema.ts) é JSON de schema fechado, sem nenhum campo numérico: o modelo escreve{{tpsa}}e quem põe o valor é a interface, a partir do que o RDKit calculou. Texto com dígito solto é recusado. - Cada bloco na tela carrega indicador de origem — verde para calculado, âmbar para gerado.
- Cache por
(inchikey, quest_slug, tipo_de_dica)emTutorHint. O mesmo erro na mesma missão não paga duas vezes — e o texto fala da química, não da pessoa. - Teto por conta por dia (
TUTOR_DAILY_LIMIT), com degradação para as dicas escritas à mão. - O modelo padrão é um apelido com reserva fixa (
GEMINI_MODELtroca sem tocar no código), porque modelo sai de circulação e o sintoma era o tutor calar como se não houvesse chave. - Missão de professor: o prompt recebe os rótulos gerados dos objetivos, nunca o enunciado que o professor escreveu (R-9).
| Onde | O que | Como roda |
|---|---|---|
packages/core/test |
os valores de referência do CLAUDE.md, grupos, estereo, geometria, dinâmica, modos, organizar |
Vitest, RDKit do pacote npm, sem navegador |
packages/quests/test |
catálogo, veredito, Assessable, extração de objetivos |
Vitest |
packages/editor2d/test |
geometria 2D, render, store, anéis | Vitest |
apps/web/lib/*.test.ts |
apelido, PubChem com respostas gravadas, código de troca de senha, prompt e schema do tutor | Vitest |
apps/web/app/actions/assignment.test.ts |
as regras das listas e do catálogo, uma a uma, contra o Postgres | Vitest; pula avisando sem banco, falha com REQUIRE_DATABASE=1 |
apps/web/e2e/*.spec.ts |
o produto no navegador, desktop e celular, contra o build de produção | Playwright, pnpm test:e2e |
O que o sistema nunca afirma
Seção intitulada “O que o sistema nunca afirma”- Que previu o produto de uma reação ou uma rota de síntese.
- Que uma molécula tem atividade biológica. Descritores são descritores.
- Que substitui PyMOL, ChemDraw ou Maestro.
- Que nomeia compostos. Deixa batizar, que é autoria de apelido, sempre com o nome de quem deu.