Guia operacional para agentes de IA e desenvolvedores que vão trabalhar neste repositório com segurança, contexto e consistência.
Última atualização: 2026-04-08 — Step Quality V1 feedback, flag toggle anti-flicker, audio pipeline unificado, input convergence.
Piano Trainer é um aplicativo web de prática de piano que guia o aluno por lições organizadas em capítulos (trails), exibindo partituras musicais (MusicXML via OSMD) e "falling notes" (piano roll) em tempo real enquanto recebe input MIDI do teclado do usuário.
Permite praticar piano com feedback imediato (HIT/MISS/LATE), rastreamento de progresso, analytics de desempenho e gamificação (streaks, scores, stars, badges).
| Camada | Tecnologia |
|---|---|
| Frontend | React 18, TypeScript, Vite 5, Tailwind CSS |
| UI Components | Radix UI (shadcn), Framer Motion, Recharts |
| Auth & DB | Lovable Cloud (Supabase managed) |
| MIDI | Web MIDI API (webmidi-service.ts) |
| Sheet Music | OSMD (OpenSheetMusicDisplay) |
| Testes | Vitest + jsdom (370+ testes, 36 arquivos) |
Nota: Este projeto consome um backend FastAPI externo em
api.devoltecomele.comvia Edge Function proxy (api-proxy). O catálogo, sessões e analytics vêm do backend (fonte única de verdade). O proxy resolve CORS em preview e produção.assets/lessons.jsoné usado apenas para indexação estática de metadados de trail chapters.
src/viewer/— Núcleo funcional: motores de lição (V1/V2), MIDI, piano roll, OSMD, transport, analytics, auth, endscreen, feature flags, catálogo.src/viewer/catalog/— Camada de catálogo: tipos, adapter local, serviço centralizado.src/config/— Configuração desacoplada de runtime (app-config.ts).src/components/+src/pages/— UI React convencional (dashboard, lessons hub, login, settings).viewer/(raiz) — LEGADO. Cópia antiga do viewer com build independente. NÃO é o entrypoint.src/viewer/é canonical. Candidata a remoção.
[Usuário] → MIDI Keyboard → WebMidiService → LessonEngine (V1 ou V2)
↕
[Backend API] → api-proxy (Edge Function) → proxyFetch → CatalogService → Trail[] → TrailNavigator/Hub UI
↕
[OSMD] → Partitura renderizada → beat-to-x-mapping → Falling Notes / Cursor
↕
[Auth] → Lovable Cloud (Supabase externo) → token → x-external-auth → backend
| Caminho | Responsabilidade |
|---|---|
src/main.tsx |
Entrypoint real. Carrega loadRuntimeConfig() → valida → importa src/viewer/index.tsx |
src/viewer/index.tsx |
Orquestrador principal (~3000 linhas). Monta DOM, inicia MIDI, transport, engine, renderiza Home/Hub/Dashboard/Trainer. |
src/viewer/catalog-service.ts |
Serviço central de catálogo. Carrega do backend via proxyFetchJson('/v1/catalog'). Sem fallback local — backend é fonte única. Indexa metadados estáticos de lessons.json para TrailNavigator. |
src/viewer/catalog/types.ts |
Tipos do catálogo: Trail, TrailLevel, TrailModule, TrailChapter, HandAssignment. |
src/viewer/catalog/adapter.ts |
Adapter: converte catálogo local → Trail[] hierárquico. |
src/viewer/catalog/local-catalog.ts |
Builder: lê assets/lessons.json e monta estrutura normalizada (tracks[], chapters[], lessons[]). |
src/viewer/components/TrailNavigator.tsx |
Componente completo de navegação: overlay com level tabs, módulos acordeão (framer-motion), cards de capítulo com badges/progresso, card "Recomendado", hand badges. Usa classes CSS de styles.css. |
src/viewer/lesson-engine.ts |
Motor de lição V1 (monofônico) e V2 (polifônico/acordes). WAIT + FILM modes. |
src/viewer/lesson-pipeline.ts |
Parser + roteador automático V1/V2 baseado em heurística. |
src/viewer/beat-to-x-mapping.ts |
Mapeia beat musical → posição X na tela (critical para falling notes + cursor). |
src/viewer/analytics-client.ts |
Cliente de analytics: fetchOverview() via proxyFetch, cache por user, fallback estático configurável. |
src/viewer/auth-storage.ts |
Extração de token JWT de múltiplas chaves de storage (legado + dinâmico Supabase). |
src/viewer/auth/ |
Auth gate: login/registro via Supabase. Non-blocking — app funciona sem sessão. |
src/viewer/transport/ |
Abstração REST/WebSocket (factory.ts detecta automaticamente). |
src/viewer/services/taskCompletion.ts |
Cálculo de resultado (score, stars, high score, per-note stats). |
src/viewer/services/lesson-transposer.ts |
Transposição imutável de lições (clamp MIDI 21-108). |
src/viewer/feature-flags/ |
Feature flags com 4 camadas: default → localStorage → remote → runtime. |
src/viewer/components/Endscreen/ |
Tela de resultado pós-lição. |
src/viewer/lesson-timer.ts |
Timer com start/stop/reset, tick a cada 100ms. |
src/config/app-config.ts |
Configuração centralizada: window.__APP_CONFIG__ → /config.json → import.meta.env. |
src/hooks/useLessons.ts |
Hook React: buildLocalCatalog() → adaptCatalogToTrails() → Trail[]. |
src/pages/LessonsHubPage.tsx |
Página de catálogo React: consome useLessons() e renderiza capítulos reais. |
src/viewer/__tests__/ |
32 arquivos de teste Vitest cobrindo regressões críticas. |
public/config.json |
Config de runtime (Supabase URL, analytics mode). |
assets/ |
Metadados estáticos do currículo. lessons.json usado para indexação de trail chapters. Não é mais fonte primária — backend é fonte única. |
src/viewer/proxy-fetch.ts |
Utilitário centralizado de fetch via proxy. Todas as chamadas /v1/* passam por aqui → Edge Function api-proxy → backend. Injeta x-external-auth + apikey. |
supabase/functions/api-proxy/index.ts |
Edge Function proxy genérica. Encaminha qualquer método/path para api.devoltecomele.com. Resolve CORS. |
viewer/ (raiz) |
LEGADO. Não usar. src/viewer/ é o canonical. |
Backend API (api.devoltecomele.com)
→ Edge Function api-proxy # supabase/functions/api-proxy/index.ts
→ proxyFetchJson('/v1/catalog') # src/viewer/proxy-fetch.ts
→ CatalogService.load() # src/viewer/catalog-service.ts
→ { tracks[], chapters[], lessons[] }
→ buildTrailsFromCatalog() # Monta Trail[] a partir de tracks + chapters
→ TrailNavigator / LessonsHubPage / piano-pro-hub
// Metadados estáticos (lessons.json) usados apenas para indexação
// de TrailChapter metadata (hand, difficulty, etc.)
Frontend (proxyFetch)
→ GET/POST https://{supabase-url}/functions/v1/api-proxy/v1/{path}
Headers: apikey (anon), x-external-auth (Bearer token externo)
→ Edge Function api-proxy
→ Forward para https://api.devoltecomele.com/v1/{path}
Headers upstream: Authorization (from x-external-auth), Content-Type, Idempotency-Key
→ Response: status + body repassados com CORS headers
- Web (Lovable/Vite):
index.html→src/main.tsx→src/viewer/index.tsx
lesson-engine.tsnão depende de DOM/React — é testável isoladamente.beat-to-x-mapping.tsdepende deOsmdController(DOM) — difícil de testar unitariamente sem mock.analytics-client.tsdepende deauth-storage.ts→getAuthTokenFromStorage()e deproxyFetch.catalog-service.tsdepende deproxyFetchJsonpara carregar do backend. Retorna[]se backend indisponível.proxy-fetch.tsdepende desupabase/client.ts(URL) eauth-storage.ts(token externo).index.tsxé o "god file" que conecta tudo — modificar com extremo cuidado.
- Node.js (18+) + npm
npm install| Variável | Onde | Propósito |
|---|---|---|
VITE_SUPABASE_URL |
.env (auto-gerado) |
URL do Lovable Cloud |
VITE_SUPABASE_PUBLISHABLE_KEY |
.env (auto-gerado) |
Anon key |
VITE_SUPABASE_PROJECT_ID |
.env (auto-gerado) |
Project ID |
NUNCA editar
.envmanualmente. É gerenciado pelo Lovable Cloud.
npm run dev # Vite dev server (porta 8080)
npm run build # Build de produção
npx vitest run # Executar todos os testesnpm run build # Produçãonpx vitest run # Suíte inteira (339+ testes)
npx vitest run src/viewer/__tests__/ # Apenas testes do viewer
npx vitest run src/viewer/__tests__/polyphony # Arquivo específico
npx vitest --watch # Watch mode| Arquivo | Cobertura |
|---|---|
auth-storage.test.ts |
Token extraction, sync, clear, nested structures |
auth-storage-senior.test.ts |
Custom domain fallback, atomicidade do sync |
analytics-client.test.ts |
buildHeaders real, fetchOverview, cache, fallback |
badge-independence.test.ts |
MIDI vs Backend badges independentes |
beat-to-x-mapping-fallbacks.test.ts |
Monotonicidade, fallback triggers |
boot-state-machine.test.ts |
Boot state transitions, failure path guard, config validation |
bootstrap-regression.test.ts |
Boot shell, auth gate z-index, single init guard, route activation |
catalog-service.test.ts |
Cache, dedup, chapter→lesson mapping, indexação estática, fallback local |
complete-payload-invariants.test.ts |
local_date São Paulo, fire-once guard |
dashboard-ux-regression.test.tsx |
Dashboard UX rendering |
feature-flags-layers.test.ts |
Precedência de 4 camadas, JSON corrompido |
feature-flags-step-quality-menu-regression.test.ts |
Toggles de Step Quality no menu UI |
feature-flags-subscribe.test.ts |
Subscribe reativo no flag store |
fire-and-forget-complete.test.ts |
POST /complete resiliente a falhas |
hand-split-rule.test.ts |
C4 (60) = mão direita |
hud-score-visibility-regression.test.ts |
Score sticky visibility, não desaparece em FINISHED |
hud-status-priority-regression.test.ts |
FINISHED terminal, não sobrescrito por HIT/WAITING |
hud-streak-combo-regression.test.ts |
Streak sticky, reset sem esconder, não some em FINISHED |
lesson-engine-invariants.test.ts |
Score, streak, AttemptLog, forceEnd |
lesson-engine-timer-integration.test.ts |
Integração engine + timer |
lesson-session-controller.test.ts |
Controlador de sessão |
lesson-timer.test.ts |
Timer unitário |
lesson-timer-regression.test.ts |
Timer básico com fake timers |
midi-onboarding-controller.test.ts |
MIDI onboarding flow controller |
midi-onboarding-runtime.test.tsx |
MIDI onboarding runtime React |
midi-onboarding.test.ts |
MIDI onboarding integration |
polyphony-chords.test.ts |
Chord expansion, PARTIAL_HIT, miss window |
progress-index.test.ts |
Progress index calculations |
step-quality-engine.test.ts |
Step quality classification engine |
step-quality-ui.test.ts |
Step quality UI controllers |
step-quality-wiring-regression.test.ts |
Step quality wiring guards |
task-completion-v2-scoring.test.ts |
V2 scoring contract |
timer-regression-end-state.test.ts |
shouldStartTimer guard, timer pós-ended |
transposition-pipeline.test.ts |
clampMidi, V1/V2, imutabilidade |
- Capítulos aparecem no Hub (LessonsHubPage ou TrailNavigator)
- Agrupamento por trilha/nível/módulo correto
- Capítulos com
coming_soonnão quebram - Capítulos sem metadados opcionais renderizam
- Seleção de capítulo resolve
chapterId → lessonId - App funciona 100% sem backend
- Endscreen aparece mesmo com falha de rede
- TypeScript strict (exceto
src/viewer/index.tsxque usa@ts-nocheck) - Imports com alias
@/parasrc/ - Nomes de arquivo: kebab-case (
lesson-engine.ts,beat-to-x-mapping.ts) - Componentes React: PascalCase (
EndscreenV2.tsx,TrailNavigator.tsx) - Testes:
__tests__/com sufixo.test.ts
- Lógica pura em arquivos dedicados (
lesson-engine.ts,lesson-transposer.ts,auth-storage.ts) - Catálogo em
src/viewer/catalog/(tipos, adapter, local-catalog) +catalog-service.ts - Efeitos colaterais concentrados em
index.tsx(o orquestrador) - Configuração sempre via
app-config.ts(nunca acessarimport.meta.envdireto em outros arquivos) - Estado de storage (localStorage) isolado por chaves com prefixo (
stats_cache_v1_,hs_,bt_)
- Extrair lógica pura em módulo testável
- Escrever testes Vitest antes de integrar em
index.tsx - Usar feature flag se o comportamento for experimental
- Manter imutabilidade (especialmente em transposição e engine)
- Reproduzir com teste unitário (Given/When/Then)
- Aplicar fix mínimo
- Verificar que teste passa
- Checar regressões nos testes existentes
index.html
→ body.app-booting (CSS hides #app, shows #boot-splash)
src/main.tsx
→ Creates window.__appBoot__ { ready(), fail(), getState() } # SOLE OWNER of boot state
→ loadRuntimeConfig()
→ validateConfig()
→ Production + config missing → THROWS (boot fails with error overlay)
→ Dev + config missing → warn + continue
→ import src/viewer/index.tsx
→ startApp()
→ ensureAuthenticated()
→ authenticated → syncSession, continue
→ unauthenticated → login overlay BLOCKS until user logs in
→ disabled (no config) → continue without auth
→ init()
→ SUCCESS → window.__appBoot__.ready() # Removes boot shell
→ FAILURE → window.__appBoot__.fail(err) + return # Stays in failed state
Decisão arquitetural:
main.tsxé o dono único do lifecycle de boot.index.tsxNUNCA manipulabody.classListoudataset.appStatediretamente — usawindow.__appBoot__. Transiçãofailed → readyé bloqueada (irreversível).
ensureAuthenticated()
→ Se config ausente: retorna { status: 'disabled' } (app continua sem auth)
→ Se sessão existe: syncSessionToLegacyStorage() → { status: 'authenticated' }
→ Se sem sessão: renderiza #auth-gate (z-index 100000) → BLOQUEIA até login → { status: 'authenticated' }
Nota: Auth é blocking para o viewer (init só roda após auth resolver), mas non-blocking no sentido de que o app funciona sem config de auth (
disabled).
init()
→ requestChapterCatalog()
→ catalogService.load() # src/viewer/catalog-service.ts
→ proxyFetchJson('/v1/catalog') # src/viewer/proxy-fetch.ts
→ Edge Function api-proxy # supabase/functions/api-proxy/index.ts
→ GET https://api.devoltecomele.com/v1/catalog
→ buildChapterLessonMap() # Indexa chapter_id → lesson_id
→ catalog cached in memory
→ catalogService.getTrails()
→ buildTrailsFromCatalog() # Monta Trail[] a partir de tracks[] + chapters[]
→ Renderiza TrailNavigator / LessonsHubPage
Decisão arquitetural: O backend é a fonte única de verdade para o catálogo. Se o backend falhar, a lista de capítulos fica vazia.
Usuário seleciona capítulo no Hub
→ getChapterLessonId() # Resolve chapter → lesson_id (local map ou fallback lesson_{id})
→ parseAndRoute() # Auto-detecta V1 ou V2
→ Se V2: pipelineV2() → createEngineV2() → loadLesson()
→ Se V1: pipelineV1() → createEngineV1() → loadLesson()
→ buildBeatToXMapping() # OSMD cursor scan → mapping
→ deriveRenderNotesFromV2Steps()
→ Inicia LessonTimer
WebMidiService.onNoteOn(midi, velocity)
→ pushEvent('note_on')
→ engine.onMidiInput(midi, velocity, true)
V2: Verifica se midi está no chord do step atual
→ Se sim e todas as notas do chord foram tocadas: HIT, avança step
→ Se sim mas incompleto: PARTIAL (não avança)
→ Se errado: MISS (reseta stepState)
→ logAttempt() no AttemptLog
→ Atualiza HUD (score, streak, feedback visual)
→ Se engine ended: forceEnd() → notifyEnded() → Endscreen
Engine ended (setupEngineEndCallback em index.tsx)
→ sessionController.endLesson("COMPLETE")
→ engine.getAttemptLog() → attempts válidos
→ engine.getCompletedSteps() → completedSteps (V2 only)
→ engine.getTotalExpectedNotes() → totalExpectedNotes (V2 only)
→ computeTaskResult(attempts, totalSteps, mode, ..., engineStats)
→ dispatchTaskCompletion(result)
→ POST /v1/sessions/{session_id}/complete (fire-and-forget, inline em index.tsx)
→ Headers: Authorization: Bearer <token>, Idempotency-Key: crypto.randomUUID()
→ Payload: { completed_at, duration_ms, summary: { pitch_accuracy, timing_accuracy, avg_latency_ms, std_latency_ms, hits, misses }, attempts_compact }
→ Guard: `completeSent` flag impede envio duplicado na mesma sessão
→ Guard: sem session_id ou sem token → skip com log
→ Falha de rede: log "[Complete] failed", NÃO bloqueia Endscreen
→ showEndscreen(result) — SEMPRE executa, independente do POST
AttemptLognão é fonte de verdade paracorrectStepsem V2.- Em lições polifônicas, MISS + retry deixam histórico no log, mas o engine já sabe quantos steps foram completados.
- Fonte de verdade:
engine.getCompletedSteps()→correctSteps(incrementa exatamente 1x por step completado)engine.getTotalExpectedNotes()→totalExpectedNotes(derivado desum(step.notes.length))
computeTaskResult()usaAttemptLogapenas para derivarcorrectNotescontando notas esperadas únicas satisfeitas (sem inflar por retries/duplicatas).- Fallback legado sem
engineStatspermanece disponível para compatibilidade de testes. - Testes:
src/viewer/__tests__/task-completion-v2-scoring.test.ts
featureFlags.init(remoteProvider?)
1. DEFAULT_FLAGS (hardcoded)
2. localStorage (LocalFeatureFlagProvider)
3. Remote provider (se configurado)
4. Runtime: window.__flags.set('showSheetMusic', false, 'runtime')
| O quê | Onde |
|---|---|
| Currículo / lições | Backend API /v1/catalog via api-proxy (fonte única) |
| Tipos do catálogo | src/viewer/catalog/types.ts |
| Tipos do domínio musical | src/viewer/types.ts |
| Tipos do task/endscreen | src/viewer/types/task.ts |
| Tipos de analytics | src/viewer/analytics-client.ts (interfaces inline) |
| Tipos de auth | src/viewer/auth/types.ts |
| Audio service | src/viewer/audio-service.ts (síntese piano-like, ADSR, compressor) |
| Serviço de catálogo | src/viewer/catalog-service.ts (carrega via proxyFetchJson) |
| Proxy centralizado | src/viewer/proxy-fetch.ts (todas as chamadas /v1/*) |
| Edge Function proxy | supabase/functions/api-proxy/index.ts |
| Adapter local (metadados) | src/viewer/catalog/adapter.ts + local-catalog.ts |
| Config de runtime | src/config/app-config.ts (AppConfig interface) |
| Config publicada | public/config.json |
| Feature flags | src/viewer/feature-flags/types.ts (FeatureFlags) |
| Engine de lição (lógica central) | src/viewer/lesson-engine.ts |
| Mapeamento beat→X | src/viewer/beat-to-x-mapping.ts |
| Transposição | src/viewer/services/lesson-transposer.ts |
| Auth storage | src/viewer/auth-storage.ts |
| HUD service | src/viewer/ui-service.ts (score/streak sticky, status priority) |
| Supabase types (auto-gerado) | src/integrations/supabase/types.ts (NÃO editar) |
| Supabase client (auto-gerado) | src/integrations/supabase/client.ts (NÃO editar) |
- Sempre ler o arquivo antes de alterar. Nunca assumir conteúdo baseado no nome.
- Nunca editar arquivos auto-gerados:
src/integrations/supabase/client.ts,src/integrations/supabase/types.ts,.env,supabase/config.toml. - Respeitar a hierarquia de config:
window.__APP_CONFIG__→/config.json→import.meta.env. Nunca lerimport.meta.envdiretamente fora deapp-config.ts. - Preferir mudanças mínimas. O princípio do projeto é "adaptar o ambiente ao app, não o app ao ambiente."
- Não renomear pastas/arquivos em
src/viewer/. A estrutura é preservada para portabilidade entre plataformas. - Testes obrigatórios ao mudar
lesson-engine.ts,auth-storage.ts,analytics-client.ts,beat-to-x-mapping.ts,lesson-transposer.ts,catalog-service.ts,ui-service.ts,audio-service.ts. - Nunca armazenar secrets em código. Usar
public/config.jsonpara chaves públicas (anon key). - Imutabilidade:
LessonTransposer.transpose()retorna clone. Engine não muta input. Manter esse padrão. - Fire-and-forget: POST
/v1/sessions/{id}/completenunca deve bloquear a UI. Falhas são logadas, não lançadas. GuardcompleteSentimpede duplicidade. - Feature flags: Novas features experimentais devem ser protegidas por flag em
src/viewer/feature-flags/types.ts. - Backend é fonte única do catálogo:
CatalogService.getTrails()retorna[]se o backend não respondeu. Não há fallback local.assets/lessons.jsoné usado apenas para metadados estáticos de trail chapters. - Todas as chamadas /v1/ via proxy:* Usar
proxyFetch()ouproxyFetchJson()desrc/viewer/proxy-fetch.ts. Nunca chamarapi.devoltecomele.comdiretamente do browser. viewer/(raiz) é legado. Sempre editarsrc/viewer/. Nunca editarviewer/.- Edge Function
api-proxyé o ponto único de saída para o backend. Não criar proxies adicionais para endpoints individuais.
- Diagnosticar: Ler logs, checar storage, validar env vars e feature flags.
- Isolar: Reproduzir com teste unitário.
- Planejar: Identificar ponto de entrada e consumidores afetados.
- Implementar: Fix mínimo + teste.
- Proteger: Adicionar teste anti-regressão que falha sem o fix e passa com ele.
- Validar:
npx vitest run(todos os testes devem passar).
- Anti-regressão é parte do processo, não pós-pensamento. Bug corrigido sem teste anti-regressão precisa de justificativa explícita.
- Nem todo bug pede refactor; muitos pedem teste cirúrgico. O objetivo é impedir que o mesmo bug reabra, não reescrever o sistema.
- Unit test sozinho não substitui teste de integração/wiring. Se o bug nasceu na camada de wiring (ex:
index.tsx), o teste precisa simular o wiring, não só o módulo isolado. - Testar comportamento observável, não detalhes internos. Acoplamento a implementação gera testes frágeis.
- Testes verdes não bastam se a área alterada não está coberta. A suíte pode estar 100% verde e o bug existir em camada sem cobertura.
| Situação | Tipo de teste mínimo |
|---|---|
Mudança em módulo crítico (lesson-engine, auth-storage, analytics-client, beat-to-x-mapping, lesson-transposer, catalog-service, taskCompletion, ui-service) |
Unit test |
| Bug fix em qualquer módulo | Anti-regression test (deve falhar sem o fix) |
| Mudança em feature flags | Teste de combinação de flags (ON/OFF matrix) |
Mudança em guards de index.tsx |
Teste de wiring simulando o contrato do handler |
| Mudança em lifecycle/boot | Teste de inicialização com flags em estados diferentes |
| Nova feature experimental | Teste de comportamento + flag gate |
| Tipo | Quando usar | Exemplo |
|---|---|---|
| Unit test | Lógica pura sem dependência de DOM/wiring | lesson-engine-invariants.test.ts |
| Integration test | Múltiplos módulos colaborando | lesson-engine-timer-integration.test.ts |
| Anti-regression test | Reproduzir bug específico e impedir reabertura | step-quality-wiring-regression.test.ts |
| Wiring test | Simular contrato do entrypoint/handler sem importar index.tsx |
step-quality-wiring-regression.test.ts (guard matrix) |
| Runtime test | Validar comportamento com DOM simulado + fake timers | step-quality-ui.test.ts |
- Entrypoints (
index.tsx): god file com closures, guards, snapshots. Bugs aqui não são detectáveis por unit tests isolados. - Feature flags: combinações de flags podem criar branches não testados. Toda nova flag deve ter teste de matrix.
- UI wiring: controllers criados condicionalmente, snapshots congelados, subscribe esquecido.
- Runtime guards:
shouldStartTimer,completeSent,isEnded, schema/mode checks. - State/lifecycle: boot, reset, destroy, re-init, timer cleanup.
Antes de considerar uma mudança "pronta":
- Bug fix → teste anti-regressão existe? Se não, justificar por que é dispensável.
- Mudança em entrypoint/wiring → integração considerada? Unit test isolado pode não capturar o bug.
- Mudança em feature flags → matrix testada? Pelo menos: ambas OFF, ambas ON, cada uma isolada.
- Mudança em guard → branch crítico testado? O guard que bloqueia execução precisa de teste que prove que bloqueia.
- Mudança em lifecycle → cleanup testado? destroy/reset com timer pendente não pode crashar.
- Suíte verde → área alterada está coberta? Verde ≠ seguro se a área não tem teste.
bug-investigator → identifica causa raiz, propõe fix mínimo
→ orchestrator → define escopo, delega implementação
→ implementer (viewer-engineer, lesson-engine-specialist, etc.) → aplica patch
→ tdd-engineer → escreve testes anti-regressão
→ regression-auditor → valida que testes realmente blindam a regressão
→ code-reviewer → valida qualidade final (código + testes)
Regras de handoff:
- Implementador não encerra sem considerar cobertura de teste.
- Se o implementador também escreve o teste,
regression-auditorvalida independentemente. code-reviewercobra qualidade de testes, não só qualidade de código.- Bug corrigido sem teste →
code-reviewerpode bloquear.
- Entendi o fluxo completo do código que vou alterar?
- Identifiquei o ponto de entrada (é em
index.tsx? Em um módulo isolado?) - Li os tipos relevantes (
types.ts,catalog/types.ts,types/task.ts)? - Li os serviços que consomem este módulo?
- Confirmei impacto nos testes existentes?
- Verifiquei se há feature flag que controla este comportamento?
- Há risco de quebrar fluxo legado (V1 vs V2)?
- Há impacto em analytics, auth, storage ou catálogo?
- O arquivo
index.tsx(2800 linhas) será afetado? Se sim, extra cuidado. - Estou editando
src/viewer/(correto) e nãoviewer/(legado)?
-
npm run buildcompila sem erros? -
npx vitest run— todos os testes passam? - Não há imports mortos ou variáveis não usadas?
- Não há hardcode de URLs, tokens ou credenciais?
- Não há regressão nos fluxos críticos (catalog, engine, timer)?
- Se adicionei lógica nova, existe teste cobrindo?
- Se alterei engine/transposer/analytics/catalog, teste de invariante atualizado?
- Documentação (AGENTS.md, CHANGELOG.md) precisa ser atualizada?
src/viewer/é canonical. A pastaviewer/na raiz é legado com build independente e pode estar desatualizada. Sempre editar emsrc/viewer/. A pastaviewer/é candidata a remoção futura.
src/viewer/index.tsxtem ~2800 linhas com@ts-nocheck. Modificar com extremo cuidado. Preferir extrair lógica para módulos dedicados antes de adicionar funcionalidade aqui.- Bugs de wiring neste arquivo não são detectáveis por testes unitários. Se algo "deveria funcionar" mas não aparece na UI, inspecionar
index.tsxprimeiro: guards, condicionais de boot, snapshots congelados, controllers não instanciados. - Diagnóstico rápido para bugs de UI/flag: verificar nesta ordem: (1) flag está ativa? (
window.__flags.snapshot()), (2) elemento DOM existe? (document.getElementById(...)), (3) controller foi instanciado? (logs de boot), (4) guard de schema/mode está bloqueando? (logs de MIDI handler).
ensureAuthenticated()resolve silenciosamente se não houver config ou sessão. O app continua normalmente. Isso é intencional — permite funcionamento offline do catálogo e navegação.
- O sistema precisa ler tokens de 5+ chaves diferentes (legado + Supabase dinâmico) em
sessionStorageelocalStorage. - A chave dinâmica (
sb-{ref}-auth-token) depende deVITE_SUPABASE_URLbater com padrão*.supabase.co. - Custom domains (ex:
auth.meudominio.com) fazem a chave dinâmica retornarnull→ cai no fallback legado. Isso é intencional.
- Eventos MIDI tardios (depois do engine DONE) podem reiniciar o
LessonTimerse o guardshouldStartTimer(isRunning, engineEnded)não for respeitado. - Nunca remover essa checagem.
- Em acordes, o step só avança quando todas as notas são tocadas.
- Se uma nota errada for tocada durante um chord parcial, é MISS e reseta o
stepState. - Duplicatas de nota são ignoradas (não contam 2x).
- Escopo: Quality badge (PERFECT/GREAT/GOOD/RECOVERED) é V2+WAIT only. Note feedback (✓/✗) funciona para V1 e V2 em modo WAIT. FILM mode não usa Step Quality (usa streak legado).
- Feature flags:
useStepQualityStreak(default:false) — ativa scoring por qualidade de step no engine.showStepQualityFeedback(default:false) — ativa feedback visual no HUD (badge, note feedback, chord closure).enableGuestMode(default:false) — permite navegação limitada sem login. Quando ativa,ensureAuthenticated()retorna{ status: 'guest' }em vez de bloquear com overlay de login.
- Classificações por step completado:
PERFECT— 0 hard errors, 0 soft errorsGREAT— 0 hard errors, ≤1 soft errorGOOD— ≤1 hard errorRECOVERED— 2+ hard errors
- Soft errors: duplicate notes, exploração inofensiva. Hard errors: notas fora do acorde.
- Streak rules (flag ON):
- PERFECT/GREAT → streak +1
- GOOD → streak mantém (ou -1 se streak ≥ 5, "damage")
- RECOVERED → streak reseta a 0
- Mid-step: se
hardErrorCount ≥ HARD_ERROR_BREAK_THRESHOLD(3), streak quebra imediatamente
- Estado:
stepQualitiesé array local do engine, não persiste em backend. - Interface:
setUseStepQuality(enabled)egetStepQualities()são obrigatórios naLessonEngineApi. - Controllers de UI:
StepQualityBadgeController,NoteFeedbackController,ChordClosureEffect(emsrc/viewer/step-quality-ui.ts).- Instanciados sempre no boot, independente do estado das flags (tolerantes a elemento DOM ausente).
- Executam feedback apenas quando
featureFlagSnapshot.showStepQualityFeedback === trueno handler MIDI.
- DOM elements:
#hud-quality-badge,#judge-feedback,#hud-step. - Armadilha histórica (corrigida 2026-03-12): controllers eram criados condicionalmente no boot e
featureFlagSnapshotera congelado no init. Mudanças de flag em runtime não tinham efeito. Fix: criação incondicional +featureFlags.subscribe()para manter snapshot vivo. - Arquivos:
src/viewer/types/step-quality.ts,src/viewer/step-quality-ui.ts,src/viewer/lesson-engine.ts, wiring emsrc/viewer/index.tsx.
-
Score e Streak usam visibilidade "sticky": uma vez que
updateHudrecebescoreTotaloustreak, o elemento fica visível permanentemente (mesmo se chamadas subsequentes omitirem esses campos, como noFINISHED). -
Status tem prioridade terminal:
FINISHED/DONEnão podem ser sobrescritos por estados transitórios (HIT,WAITING, etc.). ApenasRESETdesbloqueia o status terminal. -
RESET limpa todo o estado interno: flags de sticky visibility, último valor de score/streak, e lock terminal.
-
Armadilha histórica (corrigida 2026-04-08):
updateHud({ status: "FINISHED" })semscoreTotal/streakescondia os valores finais. O status piscava entreHITeWAITINGsem debounce. -
Toggles de Step Quality no menu:
index.htmlagora inclui grupo "Step Quality" com toggles parauseStepQualityStreakeshowStepQualityFeedback, wired emindex.tsxviafeatureFlags.set(). -
Arquivo:
src/viewer/ui-service.ts -
Testes:
hud-score-visibility-regression.test.ts,hud-status-priority-regression.test.ts,hud-streak-combo-regression.test.ts,feature-flags-step-quality-menu-regression.test.ts -
Se a taxa de match entre notas OSMD e steps for < 80%, fallbacks são acionados automaticamente.
-
A monotonicidade (x nunca diminui com beat crescente) é crítica. Se quebrar, falling notes "voltam" na tela.
-
Line breaks (sistemas diferentes na partitura) são tratados com
LINE_BREAK_THRESHOLD.
- Backend (
/v1/catalogviaapi-proxy) é a fonte única de verdade. CatalogService.getTrails()retorna[]se o backend não respondeu.assets/lessons.jsoné usado apenas para metadados estáticos de trail chapters (hand, difficulty, etc.).CatalogService.getChapterLessonId()usa o mapa indexado do backend.- Todas as chamadas passam por
proxyFetch()→ Edge Function → backend.
src/viewer/proxy-fetch.tsé o ponto centralizado de todas as chamadas/v1/*.- Injeta
x-external-auth(token do Supabase externo) eapikey(anon key do Lovable Cloud). supabase/functions/api-proxy/index.tsencaminha paraapi.devoltecomele.comcom CORS*.- Nunca chamar
api.devoltecomele.comdiretamente do browser — sempre via proxy.
local_datedeve ser calculado emAmerica/Sao_Paulo, não UTC.- O cache de analytics é isolado por
subdo JWT. Se o sub mudar, cache antigo é descartado.
- Flags atuais:
showSheetMusic,showFallingNotes,showNewCurriculum,showIntermediateCurriculum,useWebSocket,useStepQualityStreak,showStepQualityFeedback,enableGuestMode. - Precedência:
DEFAULT_FLAGS→ localStorage (viewer:featureFlags:v1) → remote provider → runtime (window.__flags.set(...)). featureFlagSnapshotemindex.tsxé mantido atualizado viafeatureFlags.subscribe(). Mudanças em runtime refletem imediatamente no handler MIDI.- Podem ser alteradas em runtime via
window.__flags.set(...)(apenas em DEV).
AudioService(src/viewer/audio-service.ts) usa síntese layered (triangle + harmonics) com compressor dinâmico.- Áudio é tocado centralmente em
handleNoteInput()— mouse, keyboard e MIDI convergem para o mesmo ponto. piano-roll-controller.tsNÃO toca áudio diretamente (removido para evitar double-trigger).- Auto-play de falling notes controlado por
audioService.getAutoPlayFalling()(OFF por padrão).
| Termo | Significado |
|---|---|
| V1 | Schema de lição monofônico (1 nota por step). LessonNote com midi: number. |
| V2 | Schema de lição polifônico (acordes). LessonStepV2 com notes: number[]. |
| WAIT mode | Modo de prática onde o tempo para até o aluno tocar a nota correta. |
| FILM mode | Modo de prática em tempo real — notas descem e o aluno precisa tocar no timing certo. |
| Step | Unidade atômica de avaliação: 1 nota (V1) ou 1 acorde (V2). |
| Trail | Trilha de aprendizado: conjunto de levels → modules → chapters → lessons. |
| Chapter | Unidade de progressão. Cada chapter tem um default_lesson_id. |
| TrailNavigator | Componente que renderiza a hierarquia Trail[] para navegação. |
| Adapter | Camada que converte dados locais (lessons.json) para o formato Trail[]. |
| OSMD | OpenSheetMusicDisplay — renderizador de partituras MusicXML. |
| Beat-to-X mapping | Correspondência entre posição temporal (beat) e posição visual (pixels). |
| Falling notes | Visualização piano-roll: notas "caem" no canvas. |
| AttemptLog | Array de tentativas do aluno (midi, expected, success, responseMs). |
| HIT/MISS/LATE | Resultados de avaliação por nota/step. |
| PARTIAL_HIT | Estado intermediário: parte do acorde foi tocada mas não todas as notas. |
| Fire-and-forget | Padrão de POST que não bloqueia a UI em caso de falha. |
| Cold start | Primeiro carregamento do app. |
| Endscreen | Tela de resultado pós-lição (score, stars, high score, per-note stats). |
| Transport | Camada de comunicação com backend (REST ou WebSocket). Opcional neste projeto. |
| Feature flag | Toggle de funcionalidade com 4 camadas de precedência. |
| HandAssignment | 'right' | 'left' | 'both' | 'alternate' — qual mão o capítulo foca. |
- ✅ Catálogo local funcional via
assets/lessons.json→ adapter →Trail[] - ✅ Auth non-blocking — app funciona sem sessão
- ✅
CatalogServicecomgetTrails(),getTrailChapter(),getChapterLessonId() - ✅
TrailNavigatorreescrito com UI rica: level tabs, módulos acordeão, card recomendado, badges de mão, progresso - ✅
useLessons()hook consumindo pipeline local - ✅
LessonsHubPageexibindo catálogo real - ✅ Design system CSS consolidado em
src/viewer/styles.css(neon glassmorphism, variáveis CSS, responsivo) - ✅ POST
/v1/sessions/{id}/completefire-and-forget implementado no write path (index.tsx) - ✅ Step Quality System (PR1): classificação PERFECT/GREAT/GOOD/RECOVERED, streak por qualidade de step, feature flag
useStepQualityStreak - ✅ Step Quality UX/HUD (PR2): controllers visuais (badge, note feedback, chord closure), feature flag
showStepQualityFeedback, wiring corrigido para lifecycle reativo - ✅ API Proxy genérico (
api-proxy): todas as chamadas/v1/*passam pela Edge Function, resolvendo CORS em preview e produção - ✅ Backend como fonte única do catálogo:
CatalogServicecarrega do backend viaproxyFetchJson, sem fallback local - ✅
proxyFetchcentralizado: utilitário único para todas as chamadas REST ao backend - ✅ Bootstrap determinístico (2026-04-08): boot shell (
app-booting), guard de inicialização única, auth gate z-index, sem flicker de UI - ✅ Boot state machine (2026-04-08):
window.__appBoot__como dono único do lifecycle,failed → readybloqueado, config inválida em prod é fatal - ✅ HUD UX fixes (2026-04-08): score/streak sticky visibility, status terminal priority, Step Quality flag toggles no menu
- ✅ Audio pipeline unificado (2026-04-08): síntese piano-like (layered oscillators + compressor), áudio centralizado em
handleNoteInput, auto-play falling notes gated, 27 testes anti-regressão - ✅ Input convergence (2026-04-08): mouse, keyboard e MIDI alimentam o mesmo
handleNoteInput→ mesma pipeline de engine + áudio + Step Quality - ✅ Step Quality V1 feedback (2026-04-08): note feedback (✓/✗) agora funciona para lições V1 quando
showStepQualityFeedbackativo. Quality badge permanece V2-only. - ✅ Flag toggle anti-flicker (2026-04-08): subscriber de feature flags só reconstrói sheet/pianoRoll quando
showSheetMusic/showFallingNotesrealmente mudam. 10 testes anti-regressão.
viewer/(raiz): Pasta legado inteira.src/viewer/é canonical.supabase/functions/catalog-proxy/: Substituída peloapi-proxygenérico.- Arquivos
.mdde análise na raiz:ANALISE-ARQUIVOS-LEGADOS.md,RESUMO_EXECUTIVO_CTO.md,ROADMAP.md, etc. — podem estar desatualizados. run_legacy_temp.py: Script temporário sem propósito documentado.
- Cobertura de testes:
index.tsx(~3000 linhas, o orquestrador principal) não tem cobertura direta de testes. beat-to-x-mapping.ts: Testes cobreminterpolateBeatToX(pura) mas não funções dependentes de OSMD/DOM.- UX de loading/error: Catálogo, sessões e analytics não têm skeleton/error states visuais.
- Timeout no proxy:
proxyFetchnão temAbortControllercom timeout explícito. - Cache do catálogo: Sem persistência em sessionStorage; cada visita ao hub faz nova requisição (~1.5s).