| name | backend |
| description | Convenções do backend deste projeto (server/) — Express 5 + TypeScript em arquitetura hexagonal com embeddings locais e Qdrant. Use ao criar/alterar rotas, use-cases, ports, adapters, fontes de vagas, stores, entidades de domínio ou o container de DI. Cobre onde cada tipo de código mora, o fluxo domain → application → adapters, nomenclatura kebab-case.tipo.ts e o passo-a-passo para adicionar uma fonte de vagas.
|
Backend — Express + arquitetura hexagonal (server/)
RAG sobre currículo + varredura de vagas. Embeddings rodam localmente (ONNX,
Xenova/multilingual-e5-small), Qdrant é o vector DB, pesos/settings persistem em .data/.
Regra de ouro das camadas (nunca viole a direção das dependências)
domain/ → não importa NADA externo (zero deps). Só entities, value-objects, services puros.
application/ → conhece apenas ports (in/out) e domain. Nunca importa adapters concretos.
adapters/ → implementam ports. Podem usar libs externas.
config/container.ts + server.ts → únicos que conhecem implementações concretas (DI manual).
Se um use-case precisa de algo externo (HTTP, DB, IA), ele depende de um *Port de
ports/out, e o adapter concreto é injetado no container.ts. Nunca faça import de um
adapter dentro de application/ ou domain/.
- ports/in = driving (o que a aplicação oferece; rotas chamam isso). Ex.:
CrawlJobsPort.
- ports/out = driven (o que a aplicação precisa do mundo externo). Ex.:
EmbeddingPort, VectorStorePort, JobSourcePort.
- use-cases implementam um
*Port de entrada e recebem *Port de saída pelo construtor.
Nomenclatura (obrigatória — ver memória kebab-case-file-naming)
Arquivos: kebab-case.tipo.ts. A classe/interface exportada termina com o papel do sufixo:
| Sufixo do arquivo | Exporta | Onde |
|---|
.port.ts | *Port (interface) | application/ports/{in,out} |
.usecase.ts | *UseCase (implementa o *Port in) | application/use-cases |
.store.ts | *Store | application/stores |
.adapter.ts | adapter concreto (fontes: *JobSource) | adapters/out/** |
.entity.ts / .value-object.ts / .service.ts | domínio puro | domain/** |
.routes.ts | função *Routes(deps): Router | adapters/in/http/routes |
.middleware.ts | middleware Express | adapters/in/http/middlewares |
Constantes centralizadas: domain/constants.ts, application/constants.ts. Não espalhe
literais mágicos — importe de lá (COLLECTIONS, TECH_TERMS, JOB_ID_NAMESPACE, etc.).
Padrões concretos do código
- Rotas são funções que recebem os ports por parâmetro e devolvem um
Router
(jobsRoutes(crawlJobs, listJobs)). Validação de body/query com zod na própria rota
(crawlBodySchema.parse(req.body ?? {})). Erros sobem para o error.middleware.ts —
não faça try/catch por rota.
- HTTP externo sempre via
adapters/out/job-sources/support/http.ts (fetchJson/fetchHtml):
já tem timeout (AbortSignal.timeout) e user-agent. Não chame fetch cru num adapter.
- Crawl roda as fontes com
Promise.allSettled — falha de uma fonte não derruba as
outras; o resultado reporta bySource com { fetched } ou { error }.
- Normalização (em
crawl-jobs.usecase.ts): dedup por URL, filtro brazilOnly,
classifySeniority, e descarta nível com peso 0 antes de gerar embedding. IDs de
vaga são uuidv5(url, JOB_ID_NAMESPACE) (determinístico → dedup entre crawls).
- Embeddings:
embed(texts, 'passage' | 'query'). Vagas indexam como passage; a
busca de matches usa o vetor-perfil (média normalizada dos chunks do currículo).
- Stores (
weights, settings) recebem o valor inicial + um callback de persistência
no construtor; o container.ts liga isso ao FileState (.data/*.json).
Adicionar uma fonte de vagas (fluxo mais comum)
- Crie
server/adapters/out/job-sources/<fonte>-job-source.adapter.ts exportando uma
classe class <Fonte>JobSource implements JobSourcePort com readonly name e
search(query): Promise<RawJobPosting[]>.
- Busque via
fetchJson/fetchHtml do support/http; limpe HTML com stripHtml do
support/text. Mapeie para RawJobPosting (title, company, description, url, source:
this.name, location, salary?, postedAt?).
- Registre a instância em
buildJobSources() no server/config/container.ts. Se exigir
chave, siga o padrão condicional (push só se a env existir, senão console.warn).
- Nada mais muda — normalização, embedding, dedup e ranking são agnósticos à fonte.
Antes de dar como pronto
npm run typecheck && npm run lint
Sem quebrar a direção de dependências acima. Se precisou importar um adapter em
application/ ou domain/, o desenho está errado — extraia um port.