npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@trywhet/cli

v0.1.0

Published

Whet — static linter for AI system prompts. Detects patterns that degrade agent behavior and generates a rewrite meta-prompt for correction. Supports PT/EN/ES.

Downloads

81

Readme

Whet

Pessoas não são boas em prompt. Muitas vezes, sequer querem escrever prompts elaborados. Mas o comportamento da IA depende diretamente de como você a orienta — e a diferença entre uma orientação boa e uma ruim pode ser a diferença entre um resultado útil e uma experiência frustrante.

Poucas pessoas acumularam experiência suficiente para saber quais orientações funcionam e quais não funcionam. Whet destila essa experiência em algo que qualquer pessoa — ou agente — pode usar.

O que o sistema faz

O usuário cola seu prompt. O sistema detecta padrões que, pela experiência, degradam o comportamento de agentes de IA. O resultado é um prompt reescrito, entregue direto no navegador.

A análise é 100% local (client-side, offline). A reescrita é opcional e acontece em um clique: o sistema manda a instrução de correção para uma das IAs gratuitas parceiras — Gemini, Mistral ou Llama via Groq — com rotação LRU entre elas para amortizar os free tiers, e devolve o prompt reescrito com atribuição explícita ("Reescrito por Gemini 2.5 Flash") e delta de score (86 → 100, com count-up animado).

Por baixo, o que alimenta a reescrita é o meta-prompt de reescrita — uma instrução de correção autocontida que inclui o prompt original, lista as adequações sugeridas, e instrui o destinatário a preservar o propósito original de cada instrução, descartando o que não fizer sentido. Ele não some: fica acessível a um clique num acordeão "Ver instrução de correção", pra quem quer auditar o porquê ou usar o fluxo manual (copiar e colar numa IA de preferência, inclusive modelos locais).

Três caminhos, um mesmo core:

  • One-click (default) — clique em "Reescrever com IA", o rotador escolhe uma IA gratuita disponível, o prompt reescrito aparece com animação materializando o container
  • Manual (fallback automático) — se todas as IAs gratuitas estão exauridas, ou se o usuário preferir, o painel degrada pra mostrar a instrução de correção copiável
  • Educativo (sempre disponível) — diagnósticos, sugestões, dicas de reformulação e razões de cada detecção ficam visíveis sob o painel principal, independente do caminho escolhido

O que a experiência mostra sobre instruções

Estes padrões vêm de situações reais, não de teoria. Se não há uma história real por trás de uma regra — um momento onde a IA se comportou de forma inadequada ou surpreendentemente boa — essa regra não deveria existir. Isso vale para as instruções que o linter analisa e para as regras do próprio linter.

Uma instrução ruim é pior do que nenhuma instrução

Um prompt mal escrito não é neutro — o agente gasta esforço tentando cumprir regras em vez de atender ao propósito real de quem está usando. Instruções que restringem demais podem acabar sendo piores do que nenhuma instrução — porque a IA gasta energia avaliando regras em vez de ser produtiva.

Orientar sobre o que não funciona vale mais do que dizer o que fazer

Instruções positivas ("seja útil", "responda com clareza") quase sempre repetem o que o modelo já faz sozinho. As instruções que mudam comportamento de verdade são as que sinalizam armadilhas — o que tende a dar errado e por quê.

Tom imperativo gera paralisia — e conflitos

Instruções rígidas ("SEMPRE faça X", "NUNCA faça Y", "É OBRIGATÓRIO", "É PROIBIDO") produzem agentes excessivamente cautelosos. Além de paralisia, geram conflitos — quando várias instruções imperativas competem, o agente não sabe qual priorizar.

Instruções que sugerem produzem agentes que entendem o propósito e têm margem para se adaptar ao contexto. A boa instrução define propósito, não procedimento.

Reforçar o que o modelo já faz é desperdício — e pode causar dano

Se o modelo já tem um comportamento por padrão, incluí-lo nas instruções não é neutro. Além de desperdiçar espaço de atenção, pode tornar o agente mais restritivo do que o desejado. Cada instrução precisa genuinamente mudar algo que sem ela não seria diferente.

Menos instruções, mais eficácia

Cada instrução adicional compete por atenção do agente, e o excesso causa paralisia ou comportamento inconsistente. Na prática, 3 a 5 instruções por bloco e no máximo 10 no total tendem a ser os limites onde a eficácia se mantém.

Restrições categóricas são diferentes de orientações comportamentais

"Use TypeScript" ou "responda em JSON" são restrições de ferramenta ou formato — binárias, legítimas, que não se beneficiam de tom sugestivo. "Sempre cite a legislação" é uma orientação comportamental — faz sentido reformular para que o agente entenda o propósito. O linter distingue as duas: restrições categóricas podem ser mantidas como estão.

Domínio importa — nem todo imperativo é igual

Em contextos onde erro gera risco real (saúde, direito, finanças), certas instruções imperativas podem ser mais justificáveis do que em outros domínios. O meta-prompt de reescrita sinaliza o domínio detectado e orienta a IA destinatária a preservar proteções que existem por razões de segurança ou compliance, reformulando para que o agente entenda o porquê em vez de apenas obedecer.


O que o linter detecta

Cada regra detecta um padrão e gera orientações que alimentam o meta-prompt de reescrita.

| Regra | Situação que endereça | |---|---| | redundant-default | Instruções que repetem comportamento que o modelo já tem, desperdiçando atenção e podendo torná-lo mais restritivo | | imperative-overload | Excesso de linguagem imperativa que tende a gerar agentes travados, cautelosos e com conflitos entre regras | | cognitive-overload | Muitas instruções competindo por atenção, além dos limites onde a eficácia se mantém (com tolerância maior para prompts estruturados — listas numeradas ou prosa com seções rotuladas em negrito) | | contradiction | Instruções que se opõem dentro do mesmo prompt — conciso vs exaustivo, formal vs informal, idioma fixo vs idioma do usuário — forçando o agente a adivinhar qual priorizar | | vague-instruction | Instruções genéricas demais para ter efeito real — o modelo interpreta como quiser | | redundant-repetition | Mesma ideia repetida com palavras diferentes, inflando o prompt sem agregar | | command-over-question | Comandos diretos e negações sem explicar o propósito — o agente adere mecanicamente em vez de entender a intenção | | threat-framing | Ameaças condicionais e framing por medo que tendem a gerar cautela paralisante em vez de orientar o modelo | | role-inflation | Inflação de credenciais no papel atribuído ao modelo (o melhor do mundo, 25 anos de experiência, prêmios internacionais) que não muda o comportamento | | conditional-reward | Promessas de gorjeta, recompensa ou avaliação positiva que são vazias para um modelo e deslocam atenção do propósito real | | tone-domain-mismatch | Tom casual/informal pedido explicitamente em domínio sensível (jurídico, saúde, finanças, contábil) onde a formalidade tem função de compliance, risco ou responsabilidade | | unresolved-reference | Instruções que remetem a artefatos externos não fornecidos no contexto do modelo (documentos anexos, templates, apêndices) que degradam silenciosamente |

Critérios para existência de uma regra

O linter não é dogmático sobre suas próprias regras — são orientações, não fórmulas. Mas cada regra precisa atender a estes critérios:

  • Não duplica comportamento que o modelo já tem por padrão
  • Tem base em experiência real, não em teoria
  • Genuinamente muda algo no comportamento
  • Funciona por si só, sem depender de outras regras
  • Tensões com outras regras, quando existem, são declaradas e genuínas

Arquitetura

O sistema tem três camadas que se comunicam numa direção só:

UI (o que o usuário vê)
 └→ Core (a inteligência — analisa o texto)
     └→ Regras (cada uma detecta um problema específico)

A camada de cima conhece a de baixo, mas a de baixo não sabe que a de cima existe. O core não sabe que está num site. As regras não sabem que existe um core coordenando. Essa independência é o que permite reutilizar o core em outros contextos depois — CLI, extensão, API — sem reescrever.

Core

Recebe texto, devolve análise. Não tem opinião sobre como o resultado vai ser exibido.

src/core/
├── models.ts       Vocabulário do sistema (Severity, Diagnostic, Rule, AnalysisResult, splitIntoStatements)
├── analyzer.ts     Coordenador — passa o texto por todas as regras, calcula score, gera output + positive traits
├── renderer.ts     Gera a instrução de correção (texto copiável) + detecção de domínio + exemplos concretos + positive traits bilíngues
├── rule-meta.ts    Metadados das regras (títulos, intros bilíngues pt/en) + utilitário groupByRule + getRuleMeta(lang)
├── i18n.ts         Dicionário de strings da UI (pt/en) — cobre labels, placeholders, mensagens, score labels, severity labels e error boundary
└── rules/
    ├── index.ts                   Registro das regras disponíveis
    ├── redundant-default.ts       Instruções que repetem comportamento padrão (padrões PT/EN/ES)
    ├── redundant-repetition.ts    Mesma ideia repetida com palavras diferentes (padrões PT/EN/ES)
    ├── imperative-overload.ts     Excesso de linguagem imperativa (gera tip; exclude estrutural cobre singular e plural; padrões PT/EN/ES)
    ├── cognitive-overload.ts      Muitas instruções ou texto longo demais (conta sentenças em blocos contínuos; prompts estruturados — listas numeradas ou prosa com ≥3 seções rotuladas em negrito — têm limite maior; padrões PT/EN/ES)
    ├── vague-instruction.ts       Instruções genéricas demais (padrões PT/EN/ES)
    ├── command-over-question.ts   Comandos diretos e negações sem propósito (gera tip; padrões PT/EN/ES)
    ├── threat-framing.ts          Ameaças condicionais e framing por medo (gera tip; padrões PT/EN/ES)
    ├── role-inflation.ts          Inflação de credenciais no papel atribuído ao modelo (gera tip; padrões PT/EN/ES)
    ├── conditional-reward.ts      Promessas de recompensa condicional ao modelo (gera tip; padrões PT/EN/ES)
    ├── tone-domain-mismatch.ts    Tom casual pedido em domínio sensível (jurídico, saúde, finanças, contábil; padrões PT/EN/ES)
    ├── contradiction.ts           Pares de instruções opostas no mesmo prompt (10 eixos: concisão vs exaustividade, formal vs informal, exemplos mínimos vs exaustivos, idioma fixo vs idioma do usuário, fontes vs sem-fontes, criatividade vs rigidez, idioma do usuário vs idioma fixo, não-inventar vs responder-com-confiança, velocidade vs profundidade, concordância vs confronto)
    └── unresolved-reference.ts    Referências a artefatos externos não fornecidos (documentos anexos, templates, apêndices) — padrões PT/EN/ES

models.ts define o vocabulário e inclui splitIntoStatements() — utilitário que divide texto em instruções individuais, suportando tanto uma instrução por linha quanto blocos de texto contínuo (split por sentença). Todas as regras usam esse utilitário. Cada Diagnostic inclui um campo highlight com a palavra/trecho específico que disparou a regra. AnalysisResult inclui positiveTraits — pontos positivos do prompt, exibidos quando não há problemas detectados.

renderer.ts gera a instrução de correção com:

  • Detecção automática de idioma (pt/en/es) — o meta-prompt de reescrita é gerado no mesmo idioma do prompt analisado
  • Detecção automática de domínio (veterinária, nutrição, saúde, contabilidade/tributário, direito, marketing, finanças, energia/petróleo, real estate/imobiliário, agricultura/agronegócio, cinema/audiovisual, educação, gestão, engenharia civil, arquitetura/urbanismo, jornalismo, meio ambiente, logística/supply chain, design/UX, programação)
  • Exemplos concretos por adequação, extraídos do próprio prompt analisado
  • Ressalva para restrições categóricas (ferramenta/formato) no closing
  • Instrução de formato: a IA destinatária deve devolver texto corrido, sem markdown, pronto para usar

Regras que geram tip oferecem uma orientação estática de reformulação, alinhada à filosofia do sistema, exibida diretamente na UI.

UI

src/app/
├── page.tsx              Componente principal (Home) — state management, encode/decode URL, header, toggle de idioma com localStorage, skip-to-content
├── layout.tsx            Layout raiz — metadados, JSON-LD structured data, wrapper global
├── not-found.tsx         Página 404 customizada bilíngue
├── opengraph-image.tsx   Geração dinâmica da imagem OG da home
├── twitter-image.tsx     Geração dinâmica da imagem Twitter card
├── globals.css           Estilos globais, animações (blur-to-sharp, section-reveal), glassmorphism
├── privacy/              Página de privacidade (rotação, limites, política de cada provider)
├── whet-benchmark/       Página pública do Whet Benchmark (abas Corpus e Ao vivo)
└── blog/                 Blog bilíngue — listagem, posts, layout, RSS, OG images
src/components/
├── SiteHeader.tsx        Header global bilíngue (Whet wordmark, toggle PT/EN, nav para benchmark e blog)
├── SiteFooter.tsx        Footer global (contato [email protected], links internos, atribuição)
├── LandingView.tsx       Tela inicial — hero com blur-to-sharp, radial glow, value props, input
├── AnalysisView.tsx      View de resultado — split view, prompt anotado (dissolve cross-panel), diagnósticos
├── RewritePanel.tsx      Painel de reescrita one-click (idle/loading/done/error) + celebração de score
├── ScoreRing.tsx         Score como anel SVG animado (CSS transition, tooltip explicativo)
├── JsonTab.tsx           Dados estruturados da análise (JSON copiável, inclui positiveTraits)
├── PromptInput.tsx       Editor com gutter de linhas e indicadores de severidade
├── ExamplePrompts.tsx    Quick-picks + painel expansível com exemplos PT/EN, triggers bilíngues
├── BenchmarkPodium.tsx   Podium visual do ranking ao vivo, exibido na landing
├── CopyEmailButton.tsx   Botão de copiar e-mail de contato (usado no footer)
├── CopyLinkButton.tsx    Botão de compartilhar link com feedback "Link copiado"
├── ErrorBoundary.tsx     Captura erros de renderização e exibe fallback amigável
├── WhetIntro.tsx         Animação transitória de entrada na landing (1x por sessão) — wordmark gigante entra borrado, sofre flash de afiamento com sparks radiais, crispa e morfa pro botão "Whet" do header via `data-whet-target`; respeita prefers-reduced-motion, `?intro=1` força replay, botão skip bilíngue, fade-out no skip, fallback gracioso se morph target não existir
├── ServiceWorkerRegister.tsx  Registro do service worker (cache stale-while-revalidate, offline)
└── blog/
    └── PostLayout.tsx    Layout compartilhado entre posts do blog (header, reading time, prev/next)
src/lib/                   Camada de backend do produto (só usada por /api/rewrite)
├── providers/
│   ├── types.ts           Contrato RewriteProvider (name, limits, isAvailable, submit)
│   ├── gemini.ts          Gemini 2.5 Flash (AI Studio free tier)
│   ├── mistral.ts         Mistral Small (La Plateforme free)
│   ├── groq.ts            Llama 3.3 70B via Groq (free tier diário)
│   └── index.ts           availableProviders() + getProviderByName()
├── rotator.ts             LRU + janelas deslizantes (rpm/rpd) + cooldown em falha
├── rate-limit.ts          Travamento por IP (10 req/hora, janela deslizante)
├── live-ranking.ts        Persistência do ranking ao vivo em Redis sorted set (fire-and-forget; nunca armazena texto do prompt)
└── usage-stats.ts         Contadores de uso no Redis (rewrites/dia, provider, país, score buckets) via pipeline
src/app/api/
├── benchmark/route.ts        GET — agrega resultados do cross-model benchmark
├── rewrite/route.ts          POST — rota principal da reescrita one-click
└── rewrite/next/route.ts     GET — consulta leve do rotador (qual provider tocaria agora)

Além das rotas /api, o blog expõe src/app/blog/rss.xml/route.ts (GET) servindo um feed RSS 2.0 dos posts.

Layout de resultado — split view

Após análise, a tela se divide em dois painéis lado a lado (empilhados em mobile):

  1. Painel esquerdo — Prompt anotado: o texto do usuário com linhas coloridas por severidade (vermelho/âmbar/azul), números de linha, e highlight inline nas palavras-gatilho que dispararam a detecção.

  2. Painel direito — Prompt reescrito (RewritePanel): quatro estados —

    • Idle: botão "Reescrever com IA" + label "próxima IA disponível: Gemini 2.5 Flash" (consulta leve a /api/rewrite/next pra saber quem vai tocar) + disclosure de privacidade + link pra /privacy
    • Loading: skeleton de tijolos montados em cascata (brickAssemble, com respiração em loop), streak de luz entrando pela esquerda como ponte causal do painel anterior, scanline ambiente em loop pra manter vivo enquanto a API responde
    • Done: prompt reescrito com atribuição (✨ Reescrito por X), celebração de score (+Δ animado, count-up, halo radial, sparkles), botões "Copiar" e "Reescrever com outra IA"
    • Error: fallback automático (all_exhausted / rate_limited / text_too_long / provider_failed) — em qualquer falha, a instrução de correção fica visível embaixo pra uso manual na IA de preferência do usuário
  3. Abaixo — Diagnósticos (progressive disclosure): uma linha curta "N padrões detectados" seguida de uma faixa de chips — um por regra detectada, colorido pela severidade, com contagem e chevron. Chips começam colapsados; click num chip expande a seção daquela regra in-place, revelando os cards completos (trecho com highlight, sugestão, dica de reformulação, razão). Click numa linha colorida do painel esquerdo expande automaticamente o grupo certo e rola até o card correspondente. Nada é escondido — só desdobrado sob demanda, pra que o resultado primário (prompt reescrito) não seja abafado por informação secundária.

Transição coreografada entre os dois painéis: quando o usuário clica "Reescrever com IA", cada linha do painel esquerdo dissolve em cascata (stagger capado em 400ms total, blur + skewX + translateX), enquanto um streak luminoso entra no painel direito simbolizando a passagem do prompt de um lado ao outro. Quando a resposta chega, o container do prompt reescrito materializa via blur-to-sharp, e a celebração de score entra 650ms depois. Se o status é done, as linhas do esquerdo voltam em modo dimmed (opacity 0.55, sem cores de severidade) pra permitir comparação visual limpa entre o original e o reescrito. Respeita prefers-reduced-motion.

A instrução de correção não some — fica acessível num acordeão "Ver instrução de correção" na base do painel direito, preservando o fluxo manual e a auditabilidade pra quem quer entender o porquê da reescrita.


Fluxo completo

1. Usuário abre o site (LandingView)
   — idioma da UI é detectado do browser, com toggle PT/EN no header para troca manual
2. Cola o system prompt na caixa de texto (PromptInput)
   — ou seleciona um quick-pick / exemplo do painel ExamplePrompts
3. Clica "Analisar" (ou Ctrl+Enter)
4. Home chama analyze(text) e salva o hash na URL
5. O analyzer percorre cada regra (splitIntoStatements divide o texto):
   ├── imperative-overload     → warnings + tip + highlight
   ├── redundant-default       → infos + highlight
   ├── cognitive-overload      → warning/error por volume (tolera prompts estruturados: listas numeradas ou prosa com ≥3 seções em negrito)
   ├── vague-instruction       → infos + highlight
   ├── redundant-repetition    → warnings por grupo semântico
   ├── command-over-question   → infos + tip + highlight
   ├── threat-framing          → warnings + tip + highlight
   ├── role-inflation          → infos + tip + highlight
   ├── conditional-reward      → infos + tip + highlight
   ├── tone-domain-mismatch    → warnings + tip + highlight (cruza domínio sensível com tom casual)
   └── contradiction           → warnings + tip (detecta pares de antônimos no mesmo prompt)
6. O analyzer junta tudo, calcula o score, gera a instrução de correção (renderer)
   ├── Detecta domínio (saúde, direito, marketing, etc.)
   ├── Gera exemplos concretos por adequação
   └── Inclui instrução de formato (texto corrido, sem markdown)
7. Transição animada para AnalysisView (split view)
8. Painel esquerdo: prompt anotado com linhas coloridas e highlights
9. Painel direito: RewritePanel em estado idle — botão "Reescrever com IA"
   + label "próxima IA disponível: X" via GET /api/rewrite/next
10. Abaixo: faixa de chips (1 por regra, colapsados por padrão); click em chip
    expande a seção daquela regra com os cards completos in-place
11. Barra superior: score ring animado + Editar prompt + JSON + Compartilhar
12. Usuário clica "Reescrever com IA"
    ├── Painel esquerdo dissolve em cascata (lineDissolve, stagger capado)
    ├── Painel direito entra em loading: streak + skeleton de bricks +
    │   scanline ambiente em loop (vivo enquanto espera)
    └── POST /api/rewrite chama o backend
13. Backend /api/rewrite:
    ├── checkRateLimit(IP) — 10 req/hora, janela deslizante
    ├── size cap (4000 chars)
    ├── gate de diagnóstico: só reescreve prompts com padrões detectados
    ├── pickNext() — LRU filtrado por cota disponível entre Gemini,
    │   Mistral e Groq (rotator com janelas de rpm + rpd + cooldown)
    ├── provider.submit(metaPrompt) — chama a IA escolhida
    ├── retry automático em outro provider se o primeiro falhar
    └── analyze(rewritten) — re-análise pra obter scoreAfter
14. Done: container materializa (blur-to-sharp), celebração entra
    ├── delta grande (+14) com gradiente e pop animation
    ├── count-up do score antes → depois (900ms, easeOutCubic)
    ├── halo radial + shimmer horizontal + sparkles
    └── atribuição "✨ Reescrito por Gemini 2.5 Flash"
15. Painel esquerdo volta em modo dimmed (sem cores) pra comparação
16. Instrução de correção disponível num acordeão "Ver instrução de correção" se o
    usuário quiser entender o porquê ou preferir o fluxo manual

A análise é 100% client-side (core roda no browser, texto nunca sai do dispositivo). A reescrita é opcional e client → backend → provider: o texto da instrução de correção viaja até o rotador e daí pra IA escolhida, sem armazenamento em nenhum ponto. Em caso de exaustão dos free tiers, o painel degrada automaticamente pra mostrar a instrução de correção copiável, preservando o fluxo clássico. A URL com hash base64 permite compartilhar análises.

Detalhes de privacidade, rotação, limites e política de cada provider em /privacy (o código da rota em src/app/privacy/page.tsx).


CLI

O mesmo core do site roda como ferramenta de terminal. Compila direto com o TypeScript do projeto — zero deps novas em runtime, só tsc em build time.

npm run build:cli              # compila src/core + src/cli para dist/
node bin/whet.js -h             # mostra a ajuda
node bin/whet.js prompt.txt
echo "Você é o melhor do mundo..." | node bin/whet.js -
node bin/whet.js prompt.txt --json

Exit code 0 quando o score é ≥ 90, 1 entre 60 e 89, 2 abaixo de 60 ou na presença de erros — feito pra servir de step num pre-commit ou CI check. Saída ANSI colorida em TTY, plain text quando a stdout é pipe/redirect. O entry point src/cli/index.ts importa direto de ../core/analyzer — nenhuma regra é reimplementada, o que dá a garantia de que produto web e CLI nunca divergem.

Stack

  • TypeScript — core do linter + web app + backend do produto
  • Next.js — interface, API routes, rotator em memória
  • Tailwind CSS v4 — estilos utilitários
  • Regras estáticas — determinísticas, sem dependência de IA
  • Providers externos — Gemini 2.5 Flash, Mistral Small, Llama 3.3 70B via Groq (rotação LRU)
whet/
├── src/
│   ├── app/           Next.js — páginas (/, /privacy, /whet-benchmark) + rotas /api (benchmark, rewrite, rewrite/next)
│   ├── core/          Engine do linter (TS puro — compartilhado entre site, API e CLI)
│   ├── lib/           Backend do produto — providers, rotator LRU, rate-limit (só consumido por /api)
│   ├── cli/           Entry point da versão de terminal (src/cli/index.ts)
│   └── components/    Componentes visuais (inclui RewritePanel)
├── bin/
│   └── whet.js        Wrapper CJS que aponta pro bundle compilado em dist/cli/
├── whorl/
│   └── benchmark/     Whet Benchmark — cross-model (corpus, providers, runner, results)
├── tests/
│   └── cli-integration.test.js  Testes de integração do CLI
├── public/
│   ├── robots.txt     Diretivas para crawlers
│   └── sitemap.xml    Mapa do site para SEO
├── package.json       Dependências, scripts e bin entry (whet)
├── tsconfig.json      Configuração do TypeScript (web)
├── tsconfig.cli.json  Configuração do TypeScript (CLI — compila só core + cli)
├── next.config.ts     Configuração do Next.js
└── README.md          Documentação do produto

Referência rápida para desenvolvimento

Setup inicial em máquina nova

git clone https://github.com/luanmadson/whet.git
cd whet
npm install                             # resolve node_modules
cat > .env.local <<EOF
PORT=3001

# === Providers de reescrita (produto) ===
GEMINI_API_KEY=...                      # https://aistudio.google.com/app/apikey (free)
MISTRAL_API_KEY=...                     # https://console.mistral.ai/api-keys (free tier)
GROQ_API_KEY=...                        # https://console.groq.com/keys (free tier diário)

# === Persistência (Redis — live ranking + usage stats) ===
REDIS_URL=...                           # Upstash ou qualquer Redis compatível com ioredis; sem essa var o ranking ao vivo é desativado silenciosamente

# === Providers extras (apenas benchmark, fora da rotação do produto) ===
DEEPSEEK_API_KEY=...                    # https://platform.deepseek.com (top-up mínimo $2, atende V3 e R1)
AI21_API_KEY=...                        # https://studio.ai21.com/account/api-key (trial gratuito — só benchmark)

EOF
npm run build:cli                       # compila CLI
npm run dev                             # sobe em http://localhost:3001

As chaves alimentam dois caminhos independentes:

  1. Reescrita one-click do produto (/api/rewrite → rotação LRU entre os providers configurados). Sem chave nenhuma, o botão "Reescrever com IA" cai direto pro estado "todas as IAs gratuitas exauridas" e o usuário só consegue copiar a instrução de correção — ou seja, o produto funciona, mas a feature principal fica dormente. Pelo menos uma chave deveria estar configurada em produção.

  2. Benchmark cross-model (whorl/benchmark/runner.js). Providers sem chave são pulados silenciosamente. A DEEPSEEK_API_KEY é exclusiva do benchmark — não entra na rotação LRU do produto porque DeepSeek deixou de ser free tier (exige top-up mínimo de $2, ver whorl/benchmark/README.md). Com uma chave configurada, o runner liga deepseek-chat (V3) e deepseek-reasoner (R1) automaticamente.

npm run benchmark:dry lista quais providers estão reconhecidos no momento — útil pra confirmar o setup sem gastar rate limit.

Produto

npm run dev              # dev server (porta definida em .env.local, default 3001)
npm run build            # build de produção (Next.js)
npm run build:cli        # compila core + cli para dist/
npm test                 # build:cli + testes de integração CLI
node bin/whet.js <arquivo>          # analisa um prompt via terminal

Dev server — operação

Cross-platform (Windows/Mac/Linux). Todos leem PORT do .env.local.

npm run check-dev        # verifica se está respondendo (exit 0/1/2)
npm run kill-dev         # mata o processo que estiver na porta
npm run fresh-dev        # kill + apaga .next + sobe dev (resolve zombies e cache corrompido)

O caso clássico de "quebrou do nada e ninguém sabe por quê" geralmente é .next/ corrompido por um next build misturado com next dev. npm run fresh-dev resolve em 1 comando. Sintomas típicos: HTTP 500 com mensagens tipo InvariantError: Expected clientReferenceManifest to be defined ou TypeError: __webpack_modules__[moduleId] is not a function.

Whet Benchmark

O Whet Benchmark mede quanto um LLM consegue afiar um prompt mal-escrito sem destruir a intenção original. É uma habilidade central da categoria de prompt-engineering-by-LLM (DSPy, OPRO, PRewrite, PromptWizard, meta-prompting) que nenhum benchmark público avalia diretamente — MMLU mede conhecimento, HumanEval mede código, needle-in-haystack mede atenção em contexto longo, τ-bench mede comportamento agentic. Meta-prompt-following sob pressão pra preservar intenção é um buraco, e o Whet Benchmark existe pra preenchê-lo.

Visível em /whet-benchmark em dois eixos complementares:

  • Corpus (default): rigoroso, rodado via runner.js sobre 9 prompts frescos por run (3 por idioma, sem repetir entre runs) em todos os providers disponíveis. O ranking é cumulativo (todas as runs, deduplicado por prompt×provider) — mede generalização real, não aderência a um set fixo. O delta scoreAfter − scoreBefore é o que se mede: quanto mais alto e estável em modelos diferentes, mais forte a evidência de que os padrões catalogados pelo linter são reais e transferíveis.
  • Ao vivo (?tab=live): ranking paralelo construído a partir das chamadas reais de /api/rewrite feitas pelos usuários do site. Persiste apenas {providerId, scores, delta, latência, timestamp}nunca o texto do prompt. Rolling 30 dias, agregação por provider com sample count explícito. Responde "sobrevive ao mundo real?". Não é same-input (o rotator LRU distribui a carga), então só vira comparável em escala.

Um provider pode dominar no Corpus (entradas controladas) e perder no Ao Vivo (prompts reais desestruturados) — esse tipo de divergência é o achado mais interessante que o sistema pode gerar. Tese completa, metodologia, limitações honestas e trajetória em whorl/benchmark/README.md.

npm run benchmark:dry                          # lista quais providers estão configurados (sem chamar APIs)
npm run benchmark                              # roda os 9 prompts do corpus atual em todos os providers
node whorl/benchmark/runner.js --providers=gemini,claude-cli
node whorl/benchmark/runner.js --prompts=prompt-a-pt,prompt-b-en
node whorl/benchmark/merge-retry.js            # mescla Run retry parcial na anterior (ver doc)

Chaves em .env.local (gitignored): GEMINI_API_KEY, MISTRAL_API_KEY, GROQ_API_KEY. Provider sem chave é pulado silenciosamente. Claude via CLI não precisa de chave — aproveita a subscription Claude Code. Metodologia completa em whorl/benchmark/README.md.

Convenções

  • Idioma do código: variáveis e tipos em inglês; comentários e documentação em português
  • Commit messages: feat(<escopo>): ... ou fix(<escopo>): ...
  • Regras novas: criar em src/core/rules/, registrar em rules/index.ts, adicionar metadados em rule-meta.ts
  • Testes: tests/cli-integration.test.js cobre o CLI; sem testes unitários para regras individuais (lacuna conhecida)