diff --git a/PROGRESS.md b/PROGRESS.md index fbdd311..556908c 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -1647,3 +1647,11 @@ Plano: `docs/superpowers/plans/2026-10-14-issue-46-avisos-e-termos.md`; achados - Os rótulos curtos da barra inferior do admin mudaram ('Áreas', 'Trilha'): decisão de produto a confirmar. - `upcomingTermsChange` morta mantida; varredura de `sending` só na partida e para instância única. (issue a abrir) - Guarda "texto novo embarcado no app" perdida (issue a abrir): `upcoming_legal_documents_test.dart` compara com `upcomingTermsChange`, constante morta desde que a agenda passou a `terms_versions`; o admin pode publicar uma versão cujo texto o app não embarca (o cartão aparece sem "Ler o texto novo"). + +## Auditoria de lacunas do PRD (2026-10-15) + +Relatório item a item de `spec/PRD_system.md` contra o código, com evidência medida a partir de `develop` em `92106d1`: +[docs/auditoria-prd/2026-10-15-lacunas-prd.md](docs/auditoria-prd/2026-10-15-lacunas-prd.md). +Contagem dos 64 IDs do PRD: 24 entregue, 27 parcial, 4 ausente, 9 externo (mais 57 linhas sem ID próprio, para SLOs, STRIDE, criptografia, acessibilidade, telemetria, métricas do §1.3 e módulos dos apps). +Os três P0 do backlog: colunas de risco em texto plano (INV-04), o estado "Atendido" que o paciente nunca vê (RF05) e o SMS real do OTP (RF01). +Backlog priorizado na seção final do relatório; conferência por `python3 scripts/qa/cobertura_prd.py --final`. diff --git a/docs/auditoria-prd/2026-10-15-lacunas-prd.md b/docs/auditoria-prd/2026-10-15-lacunas-prd.md new file mode 100644 index 0000000..281dc39 --- /dev/null +++ b/docs/auditoria-prd/2026-10-15-lacunas-prd.md @@ -0,0 +1,253 @@ +# Lacunas do PRD — auditoria de 2026-10-15 + +Fonte: `spec/PRD_system.md` no commit `73bcb0f` (branch `docs/auditoria-lacunas-prd`, a partir de `develop` em `92106d1`). Cada linha foi medida no código neste commit; +`PROGRESS.md` serviu de hipótese, não de prova. Conferência: `python3 scripts/qa/cobertura_prd.py --final`. + +**Linha de base dos testes:** backend 1355/1355 (`dart test`, com `postgres-test`), ACS 544/544, paciente 259/259, admin 459/459 (`flutter test`), scripts/qa 42 testes Python OK. Nenhuma falha. + +**Amostragem:** 3 de 3 linhas `entregue` reproduzidas do zero (RF02, RF08, M1.2; 2026-10-15). + +**Legenda de status:** `entregue` (código + prova do critério do PRD) · `parcial` (código sem o critério provado) +· `ausente` · `fora-do-mvp` (ausente e listado em v1.5+ no §6.1) · `externo` (falta só ação de terceiro). + +Linhas com prefixo `MOD-`, `STRIDE-`, `CRIPTO-`, `SLO-`, `A11Y-`, `DADOS-`, `EVT-` e `MVP-` cobrem itens do PRD sem ID +próprio; o conferidor exige apenas os IDs do PRD. + +## Paciente + +| ID | Requisito | Status | Evidência | O que falta | +|----|-----------|--------|-----------|-------------| +| RF01 | Autenticação passwordless (CPF + nascimento + OTP) | **parcial** | Fluxo CPF + nascimento + OTP em `auth.requestOtp`/`verifyOtp`; `passwordless_auth_service_test.dart` passa; e2e real em `scripts/qa/patient_full_e2e.sh` (2026-09-30). Único gateway é `LoggingSmsGateway` (`backend/sinalacs_server/lib/src/application/auth/sms_gateway.dart:36`), que só escreve o código no log em `APP_ENV=development`. | Escolher e integrar provedor de SMS (implementar `SmsGateway`); criar coluna de telefone do paciente (hoje o destino é o CPF, `sms_gateway.dart:12`). | +| RF02 | Onboarding via QR Code | **entregue** | Convite de uso único em `onboarding_endpoint.dart:21` (`generateEnrollmentToken`) e `:38` (`completeEnrollment`); `qr_flutter` no ACS, `mobile_scanner` no paciente (`apps/patient/lib/app/qr_scanner.dart`); `onboarding_service_test.dart` recusa outra microárea; `qr_scanner_test`, `onboarding_flow_test`, `enrollment_qr_test` passam (36 testes, 2026-10-15). | | +| RF03 | Botão de alerta de urgência (MQTT, GPS) | **entregue** | Botão `panic_button` 208x208 dp (`apps/patient/lib/app/app.dart:1380`); localização por `GeolocatorLocationReader` vira geocélula arredondada + hash antes do envio (`app.dart:1300-1320`); `alerts.createRedAlert` publica via outbox MQTT; ciclo provado em `red_alert_cycle_test.dart` (integração) e entrega p95 212 ms medida por `scripts/qa/measure_latency.dart` (2026-10-16 UTC). | | +| RF04 | Triagem estruturada (árvore de decisão) | **entregue** | Seis perguntas sim/não em `TriageSymptom` (`apps/patient/lib/app/app.dart:1429`) mapeadas 1:1 para `TriageEngine.evaluate` (`backend/sinalacs_server/lib/src/application/triage/triage_engine.dart`); risco calculado só no servidor. | O §6.1 fala em 3 perguntas e o código usa 6: atualizar o texto do PRD (correção do PRD, não do código). | +| RF05 | Painel de status da solicitação | **parcial** | `alerts.statusFor` + tela de status (`apps/patient/lib/app/app.dart:2518`) mostram Enviado (pending) e Recebido pela equipe (acknowledged); sincronização periódica testada em `patient_app_mvp_test.dart`. | Nenhum código leva o alerta a `resolved` (só existe no enum `alert_status.spy.yaml:8`), então o paciente nunca vê Atendido: falta a ação do ACS de encerrar o atendimento e a transição no backend. | +| RF06 | Lembretes de saúde (notificação local) | **entregue** | `flutter_local_notifications` + `apps/patient/lib/core/reminders/` (agendador e store `sqflite`); testes em `apps/patient/test/reminders/` passam. Sem endpoint por decisão §2.2.1 (§3.1). | | +| RF18 | Modo escuro nativo | **parcial** | Paciente e ACS seguem o tema do sistema (`ThemeController` com `ThemeMode.system`, `darkTheme:` em `apps/patient/lib/app/app.dart:164` e `apps/acs/lib/app/app.dart:554`), com testes de contraste nos dois temas (`contrast_tokens_test.dart`, `contrast_tokens_light_test.dart`). | O backoffice (`apps/admin/lib/app/admin_theme.dart:48`) só tem tema escuro fixo (`Brightness.dark`), sem tema claro nem `themeMode`. | + +## ACS + +| ID | Requisito | Status | Evidência | O que falta | +|----|-----------|--------|-----------|-------------| +| RF07 | Login institucional (matrícula + senha) | **entregue** | `auth.loginInstitutional` (matrícula + senha Argon2id + TOTP), refresh token rotativo preso ao Keystore; `mfa_login_test.dart` passa (2026-10-15); prova no emulador API 36 em `scripts/qa/acs_keystore_bound_e2e.sh`. | Fora do critério do PRD, mas aberto: invalidar a chave com digital nova em API 30+, provar o Keystore em API 24-29, troca de senha pelo próprio ACS. | +| RF08 | Territorialização (cache da microárea) | **entregue** | Cache da microárea por dono com validade de 72 h (`apps/acs/lib/core/services/micro_area_directory.dart:9`, `apps/acs/lib/core/database/micro_area_cache_store.dart`), sobre SQLCipher; `micro_area_cache_store_test.dart` passa. | Tempo de carga (< 30 s do MVP) não medido: ver `MVP-TERRITORIO-30S`. | +| RF09 | Dashboard de priorização dinâmica | **entregue** | `AlertQueue._sort` ordena por risco e, no mesmo risco, pelo mais antigo (`apps/acs/lib/core/services/alert_queue.dart:150`); teste `deve ordenar por risco e, no mesmo risco, pelo mais antigo` (`apps/acs/test/login_flow_test.dart:422`) passa na linha de base. | Paginação de 20 itens do SLO não existe (ver `SLO-DASHBOARD`). | +| RF10 | Mapa interativo | **parcial** | Tela de mapa com `GoogleMap` (`apps/acs/lib/app/app.dart:2501`), geocélula e círculo de incerteza; `map_screen_test.dart` (4 testes) passa; `integration_test/map_flow_test.dart`. Chave por `GOOGLE_MAPS_API_KEY` (`apps/acs/android/app/build.gradle.kts:72`). | Sem clusterização de pinos (PRD §4.1 pede para 50+ pacientes); renderização < 2 s não medida; sem chave de Maps de produção provisionada. | +| RF11 | Registro rápido de visitas offline-first | **entregue** | Fila offline por dono (`apps/acs/lib/core/services/offline_visit_queue.dart`, schema v7) com lote, retry e conflito; `offline_burst_test.dart` (100 visitas, reinício, nenhuma perdida) passa; envio disparado por volta da rede (`connectivityChanges`, `apps/acs/lib/app/app.dart:72`); sync de 100 visitas p95 112 ms (`measure_latency.dart`, 2026-10-16 UTC). | Upload em segundo plano pelo SO (WorkManager fora de `spec/stack.md`); desbloquear fila com token de 7 dias expirado. | +| RF12 | Geofencing (check-in passivo) | **parcial** | `GeofencingScreen` (`apps/acs/lib/app/app.dart:3303`) calcula distância até a geocélula com GPS em primeiro plano; `scripts/qa/acs_gps_e2e.sh` e `integration_test/geofence_gps_e2e.dart`. | Check-in passivo não existe: nenhuma geofence nativa, sem localização em segundo plano, e toda visita sai com `arrivalMethod: manual` (`apps/acs/lib/core/services/backend_visit_synchronizer.dart:67-72`); escolha de plugin e texto de divulgação aguardam revisão de produto/jurídico. | +| RF13 | Escalonamento para SAMU/UBS | **entregue** | Discador do SAMU 192 e da UBS por `url_launcher` sem ligar sozinho (`apps/acs/lib/core/services/emergency_dialer.dart:4-8`); `escalation_dialer_test.dart` e `escalation_ubs_test.dart` passam. | Integração automática com o SAMU é v1.5+ (§6.1). | +| RF14 | Avisos segmentados à comunidade (push) | **parcial** | `notices.sendSegmented` (`backend/sinalacs_server/lib/src/endpoints/notices_endpoint.dart`), registro de token só com consentimento, Gorush; entrega no tray Android via FCM provada no emulador (`scripts/qa/push_e2e.sh`, 2026-09-30) e no CI quando há segredos (`scripts/qa/ci_push_e2e.sh`); `notices_screen_test.dart` passa. | iOS/APNs, prova em aparelho físico e Gorush hospedado fora do Compose local. | +| RF15 | Sincronização bidirecional | **entregue** | Subida `visits.sync` e descida `visits.pull` por cursor (`backend/sinalacs_server/lib/src/endpoints/visits_endpoint.dart`), FSM em `backend/sinalacs_server/lib/src/application/sync/sync_fsm.dart`, consumo no ACS (`visit_pull_service.dart`, `sync_cursor_store.dart`); `visit_pull_service_test.dart` passa. | A coluna Dependência do RF15 no PRD ainda diz ainda não conectado ao cliente: corrigir o PRD. | + +## Núcleo, auditoria e backoffice + +| ID | Requisito | Status | Evidência | O que falta | +|----|-----------|--------|-----------|-------------| +| RF16 | Motor de triagem determinístico (Manchester) | **entregue** | `TriageEngine.evaluate` puro, sem IO (`backend/sinalacs_server/lib/src/application/triage/triage_engine.dart`); `triage_engine_test.dart` cobre as 64 combinações dos seis sinais (tabela-verdade completa, o que implica MCDC) mais 8 cenários nomeados; passa (2026-10-15). | O critério de 50 cenários do M1.3 está superado pelas 64 combinações; nenhum relatório de cobertura MCDC por ferramenta (ver M1.3). | +| RF17 | Logs de auditoria e conformidade | **parcial** | `audit_logs` encadeado por HMAC-SHA256; filtros, paginação, selo por linha e verificação global no servidor; exportação CSV só pelo admin, registrada na mesma transação; `audit_chain_test.dart`, `admin_endpoint_audit_chain_test.dart` e `admin_endpoint_audit_export_test.dart` passam; e2e `scripts/qa/admin_auditoria_e2e.sh` (2026-10-13/14). | Cadeia só detecta adulteração: falta trigger/`REVOKE` append-only no banco (nenhum nas migrações); `audit_exports` fora da cadeia; o CSV vai pela área de transferência (`apps/admin/lib/app/audit_log_screen.dart:389`), sem download; revisão do DPO sobre a exportação. | +| RF19 | Escalonamento de alerta vermelho sem confirmação | **entregue** | `admin.escalateAlert` com motivo de conjunto fechado, idempotente (`alreadyEscalated`) e auditado na mesma transação (`backend/sinalacs_server/lib/src/application/admin/alert_escalation_service.dart:134`); prazo `RED_ALERT_ESCALATION_SECONDS` validado de 30 a 3600 s (`app_config.dart:120`, `app_config_test.dart`); `alert_escalation_service_test.dart` (14 testes) e `admin_endpoint_escalation_test.dart` passam; e2e no emulador 2026-10-12. | O PRD ainda marca RF19 como proposto: atualizar a linha. | +| RNF06 | RBAC | **parcial** | `Authorization.require` e `AdminScope` (UBS do coordenador) com recusa auditada; `authorization_test.dart` e `admin_scope_test.dart` passam; endpoints de dados do staff existem desde a #40 (`admin_endpoint.dart`). | Matriz §4.2.2 não cumprida em duas células: o paciente não lê os próprios logs de auditoria (`PatientDataOverview` não traz acessos) e o acesso do coordenador a outra microárea não pede justificativa. O texto do RNF06 no PRD (issue #40 aberta) está velho. | + +### Módulos do backoffice (§2.1) + +| ID | Requisito | Status | Evidência | O que falta | +|----|-----------|--------|-----------|-------------| +| MOD-UBS | Gestão de UBS | **ausente** | Nenhum endpoint cria ou edita UBS: `ubs_endpoint.dart` só tem `myContact`; nenhuma tela no `apps/admin/lib/app/`. | Cadastro/edição de UBS no backoffice, incluindo o telefone da UBS usado pelo botão do ACS (hoje só em seed/SQL). | +| MOD-ACS | Gestão de ACS | **entregue** | `admin.createAcs`/`setAcsMicroArea`/`setAcsActive`/`resetAcsPassword`/`resetAcsMfa`/`resetStaffMfa` + `apps/admin/lib/app/admin_accounts.dart`; `admin_endpoint_accounts_test.dart` passa; e2e `scripts/qa/admin_acs_gestao_e2e.sh` (#43). | Criar coordenador e definir o `ubsId` dele pela interface (hoje só seed/SQL). | +| MOD-RELATORIOS | Relatórios | **parcial** | `admin.indicators` devolve contagem por risco, alertas vermelhos e TMRAV (`AdminIndicators.tmravSeconds`), com polling de 30 s; `admin_read_service_test.dart` passa. | Sem recorte por período, sem exportação de relatório e sem as métricas de produto do §1.3 além do TMRAV (sincronização, aderência, visitas por ACS/dia, retenção). | +| MOD-AUDITORIA | Auditoria | **parcial** | Tela `apps/admin/lib/app/audit_log_screen.dart` com filtros, selo e verificação global (#45). | Ver RF17. | +| MOD-CONFIG | Configurações de sistema | **ausente** | Nenhum endpoint nem tela de configurações do sistema; prazos (`RED_ALERT_ESCALATION_SECONDS`, `RETENTION_*_DAYS`) só por variável de ambiente. | Decidir se o admin edita configurações pela interface (§4.2.2 diz R/W); se sim, endpoint auditado + tela. | + +### Módulos do app paciente (§2.1) + +| ID | Requisito | Status | Evidência | O que falta | +|----|-----------|--------|-----------|-------------| +| MOD-MENSAGERIA | Mensageria assíncrona (tickets) | **fora-do-mvp** | Nenhum código de tickets ou mensagens entre paciente e equipe. | v1.5+ no §6.1. | +| MOD-PERFIL-CLINICO | Perfil clínico | **entregue** | `patients.myChronicConditions`/`updateChronicConditions` (`backend/sinalacs_server/lib/src/endpoints/patients_endpoint.dart:33,50`), cifrados em repouso; tela Perfil clínico (`apps/patient/lib/app/app.dart:1138,1714`); `patient_chronic_conditions_endpoint_test.dart` passa. | | + +## Invariantes e segurança + +| ID | Requisito | Status | Evidência | O que falta | +|----|-----------|--------|-----------|-------------| +| INV-01 | ACS só vê pacientes da própria microárea | **entregue** | Recusa de outra microárea testada por endpoint: `patients.listMicroArea` e `visits.pull` (`patient_directory_and_territory_test.dart`), `visits.sync` (`visit_sync_service_test.dart`, rejeição de território), `onboarding.generateEnrollmentToken` (`onboarding_service_test.dart:75`), `alerts.acknowledge` (`red_alert_discard_paths_test.dart`), `notices.sendSegmented` (`notices_endpoint_test.dart`), staff por UBS (`admin_scope_test.dart`); todos passam (2026-10-15). | | +| INV-02 | Classificação de risco não alterável por humano | **entregue** | Nenhum endpoint recebe risco do cliente: `createRedAlert` aceita só `idempotencyKey`, `locationHash` e `locationCell` (`alerts_endpoint.dart`); `triage.evaluate` recebe os sinais e o servidor classifica; o app do paciente não calcula risco (`apps/patient/lib/app/app.dart:1424-1428`). | | +| INV-03 | Alerta vermelho nunca perdido | **entregue** | Outbox MQTT com ACK, retry e reforço; publicação confirmada por PUBACK (`mqtt_alert_dispatcher_resilience_test.dart`, #56), caminhos de descarte cobertos (`red_alert_discard_paths_test.dart`), `red_alert_cycle_test.dart`; e2e `scripts/qa/mqtt_resiliencia_e2e.sh`; 10/10 alertas entregues na medição de latência. | O ACS só recebe alerta por MQTT: não há reserva por HTTP se o broker ficar inalcançável (ver R2). | +| INV-04 | Dado de saúde nunca em texto plano | **parcial** | Colunas clínicas cifradas em AES-256-GCM na aplicação (`*Encrypted`/`*KeyVersion` para condições crônicas, respostas de triagem e notas de visita); SQLCipher no ACS (`apps/acs/lib/core/database/encrypted_database.dart`). | Seguem em texto plano, e `spec/lgpd_data_audit.md` os classifica como dado sensível de saúde: `triage_sessions.resultRisk` e `resultDisplay` (`:50-51`), `visits.riskLevelBefore` e `riskLevelAfter` (`:64-65`) e `alerts.riskLevel` (`definition.sql` da migração `20261014155304871`). Cifrar ou registrar a decisão de deixá-los legíveis para a fila. | +| INV-05 | Paciente nunca acessa dado de outro paciente | **entregue** | Token do paciente carrega o `patientId` e os endpoints de paciente leem só dele: `myData`, consentimentos e pedidos (`patient_data_overview_endpoint_test.dart`, `data_subject_rights_endpoint_test.dart`), `statusFor` (`authorization_test.dart`, `red_alert_cycle_test.dart`), token de push por dono (`push_token_endpoint_test.dart`); passam (2026-10-15). | | + +### STRIDE (§4.2.1) + +| ID | Requisito | Status | Evidência | O que falta | +|----|-----------|--------|-----------|-------------| +| STRIDE-S | Spoofing: mensagens falsas no MQTT | **parcial** | Broker com senha + ACL por tópico + certificado de cliente obrigatório (`infra/docker/mosquitto/mosquitto.conf:10`); `scripts/qa/mtls_invariants.sh` OK (2026-10-15). | Certificado de cliente por aparelho em produção (hoje um certificado de dev compartilhado, `acs-area-12.crt`). | +| STRIDE-T | Tampering: triagem alterada no dispositivo | **parcial** | O risco é recalculado no servidor (INV-02), então adulterar a triagem no aparelho não muda a classificação. | Não há a assinatura digital dos dados de triagem no dispositivo que o PRD descreve; decidir se a reclassificação no servidor substitui a assinatura e registrar. | +| STRIDE-R | Repudiation: logs append-only assinados | **parcial** | Cadeia HMAC em `audit_logs` detecta remoção e alteração (`audit_chain_check`). | Append-only no banco (trigger/`REVOKE`) não existe: ver RF17. | +| STRIDE-I | Information disclosure: PII no dispositivo | **entregue** | SQLCipher no ACS com chave no Keystore (`apps/acs/lib/core/security/database_key_store.dart`), `FLAG_SECURE` na janela do ACS (`secure_window_test`). | O app do paciente guarda lembretes de saúde em texto livre (medicamento, pesagem: `apps/patient/lib/core/reminders/reminder.dart:1`) em `sqflite` sem cifra, por decisão; reavaliar se o campo passar a aceitar diagnóstico. | +| STRIDE-D | Denial of service: inundação MQTT | **ausente** | `infra/docker/mosquitto/mosquitto.conf` sem nenhum limite (`max_connections`, `max_inflight_messages`, `message_size_limit`); nenhum middleware de rate limit no Traefik. | Limites no Mosquitto e rate limit no Traefik. | +| STRIDE-E | Elevation of privilege: outra microárea | **entregue** | Ver INV-01 e RNF06 (`Authorization.require`, recusa auditada). | | + +### Criptografia (§4.2.3) + +| ID | Requisito | Status | Evidência | O que falta | +|----|-----------|--------|-----------|-------------| +| CRIPTO-APP-LOCAL | SQLCipher com chave derivada de PIN/biometria | **parcial** | SQLCipher com chave aleatória de 32 bytes guardada no Keystore (`apps/acs/lib/core/security/database_key_store.dart:31`); o arquivo documenta por que não usa PBKDF2 de PIN (`:8`). | Divergência do PRD (§4.2.3 pede chave derivada de PIN/biometria por PBKDF2): atualizar o PRD com a decisão ou amarrar a chave à biometria. | +| CRIPTO-APP-CACHE | SQLCipher com chave rotativa | **ausente** | A mesma chave do SQLCipher é usada por toda a vida da instalação; nenhuma rotação. | Rotação da chave do cache (mudança de dispositivo) ou decisão registrada de não rotacionar. | +| CRIPTO-APP-TRAEFIK | TLS 1.3 app ↔ Traefik | **entregue** | Traefik com `minVersion: VersionTLS13` (`infra/docker/traefik/dynamic/tls.yaml:22`); `scripts/qa/tls_invariants.sh` OK e `openssl s_client -tls1_2` na 443 recusado (2026-10-15). | Certificado Let's Encrypt em produção (hoje CA de dev). | +| CRIPTO-TRAEFIK-BACKEND | mTLS Traefik ↔ backend | **ausente** | Traefik fala com o backend em HTTP puro na 8080 (`docker-compose.yml:194`, `loadbalancer.server.port=8080`), sem `serversTransport` nem mTLS. | mTLS entre Traefik e backend, ou decisão registrada de rede interna confiável. | +| CRIPTO-POSTGRES | Criptografia no PostgreSQL | **entregue** | AES-256-GCM na aplicação com `HEALTH_DATA_ENCRYPTION_KEY` (decisão §2.2.1 §6, que substituiu pgcrypto); ver INV-04. | Chave em gerenciador de segredos (Vault) em produção; hoje variável de ambiente. | +| CRIPTO-AUDITORIA | Cadeia HMAC-SHA256 da auditoria | **entregue** | HMAC-SHA256 com `AUDIT_CHAIN_SECRET` fora do Postgres; `audit_chain_test.dart` passa. | Rotação do segredo invalida a história (limite conhecido, #45). | + +## Não funcionais + +| ID | Requisito | Status | Evidência | O que falta | +|----|-----------|--------|-----------|-------------| +| RNF01 | Latência MQTT < 500 ms (p95) | **parcial** | Entrega de alerta p95 212 ms, 10/10 amostras (meta < 500 ms); `measure_latency.dart --samples 10 --visit-count 100` contra a stack local (2026-10-16 01:27 UTC). | Só stack local, 1 cliente sequencial, 10 amostras (p95 de 10 amostras é quase o máximo): repetir em produção, com carga e mais amostras. | +| RNF02 | Sincronização offline > 99,5 % | **parcial** | Sync 10/10 lotes de 100 visitas sem perda (`measure_latency.dart --samples 10 --visit-count 100` contra a stack local (2026-10-16 01:27 UTC)); `offline_burst_test.dart` prova que nenhuma de 100 se perde em reinício. | A taxa > 99,5 % em campo não é medida: nenhum evento `sync_attempt`/`sync_result` é emitido (ver EVT-*). | +| RNF03 | Criptografia AES-256 em repouso | **parcial** | Ver INV-04 e CRIPTO-*. | Ver INV-04 (cinco colunas de risco em texto plano) e CRIPTO-APP-CACHE. | +| RNF04 | TLS 1.3 em todas as comunicações | **parcial** | RPC em TLS 1.3 obrigatório (`tls_invariants.sh` OK); broker MQTT só em TLS na 8883 com mTLS (`mtls_invariants.sh` OK). | O broker aceita TLS 1.2: `openssl s_client -tls1_2 -connect localhost:8883` com certificado de cliente negociou TLSv1.2 (2026-10-15). Falta `tls_version tlsv1.3` no `mosquitto.conf` e o caso no `tls_invariants.sh`; Traefik-backend em texto claro (CRIPTO-TRAEFIK-BACKEND). | +| RNF05 | Acessibilidade WCAG 2.1 AA | **parcial** | Matriz de contraste e `meetsGuideline` nos três apps (`apps/*/test/contrast_tokens_test.dart`, `touch_targets_test.dart` no admin), avaliação em `spec/ux_accessibility_assessment.md` (WCAG 2.2 AA, superconjunto do 2.1); linha de base verde. | Foco visível no admin (A11Y-2.4.7) e métricas de interface (A11Y-LCP/FID/CLS) não medidas. | + +### SLOs (§4.1) + +| ID | Requisito | Status | Evidência | O que falta | +|----|-----------|--------|-----------|-------------| +| SLO-LOGIN | Login < 1 s | **parcial** | Não medido. | Medir p95 do login do ACS e do paciente (o Argon2id domina o custo). | +| SLO-ALERTA | Alerta publish → ACK < 500 ms | **parcial** | p95 212 ms (meta < 500 ms) por `measure_latency.dart`, stack local, 10 amostras (2026-10-16 UTC). | Só stack local, 1 cliente sequencial, 10 amostras (p95 de 10 amostras é quase o máximo): repetir em produção, com carga e mais amostras. | +| SLO-DASHBOARD | Dashboard ACS < 1,5 s, paginação de 20 | **parcial** | Carga do dashboard não medida; a fila não pagina (lista inteira em `AlertQueue`). | Paginação de 20 itens e medição < 1,5 s. | +| SLO-VISITA | Registro offline < 200 ms | **parcial** | Inserção local sem rede (`offline_visit_queue.dart`), tempo não medido. | Medir a gravação local (< 200 ms). | +| SLO-SYNC | Sync de 50 itens < 5 s | **parcial** | Lote de 100 visitas com p95 112 ms (meta: 50 itens < 5 s), stack local, 10 amostras (2026-10-16 UTC). | Só stack local, 1 cliente sequencial, 10 amostras (p95 de 10 amostras é quase o máximo): repetir em produção, com carga e mais amostras. | +| SLO-MAPA | Mapa < 2 s, clusterização 50+ | **parcial** | Renderização não medida; sem clusterização (ver RF10). | Clusterização para 50+ pinos e medição < 2 s. | +| SLO-TRIAGEM | Cálculo da triagem < 50 ms | **parcial** | Função pura com seis booleanos (`triage_engine.dart`), sem IO; tempo não medido. | Um teste de tempo basta para fechar. | + +### Acessibilidade (§4.3) + +| ID | Requisito | Status | Evidência | O que falta | +|----|-----------|--------|-----------|-------------| +| A11Y-1.4.3 | Contraste de texto ≥ 4,5:1 | **entregue** | Matriz determinística de contraste nos três apps (`apps/acs/test/contrast_tokens_test.dart`, `apps/patient/test/contrast_tokens_test.dart`, `apps/admin/test/contrast_tokens_test.dart`) mais os de tema claro; linha de base verde. | | +| A11Y-1.4.11 | Contraste não textual ≥ 3:1 | **parcial** | Tokens de preenchimento conferidos na matriz de contraste. | Não há golden tests (o PRD pede validação visual por golden). | +| A11Y-2.5.1 | Alvos de toque ≥ 48 dp | **entregue** | `androidTapTargetGuideline` no ACS e no paciente, alvos >= 48 dp no admin (`apps/admin/test/notices_a11y_test.dart:22`); botões de 48x52 dp no paciente. | | +| A11Y-2.5.6 | Botão de pânico ≥ 60 dp | **entregue** | Botão de pânico 208x208 dp (`apps/patient/lib/app/app.dart:1380`) e SAMU 60x60 dp no ACS. | | +| A11Y-2.4.7 | Foco visível | **parcial** | Nenhum `FocusNode`, `focusColor` ou estilo de foco no `apps/admin/lib` (0 ocorrências); apps móveis usam o padrão do Material. | Indicador de foco visível no backoffice (uso com teclado no desktop/web) e teste. | +| A11Y-3.3.2 | Rótulos e Semantics | **entregue** | `Semantics`/`liveRegion` em status dinâmicos e `labeledTapTargetGuideline` (`apps/acs/test/login_flow_test.dart`, `apps/patient/test/patient_app_mvp_test.dart`). | | +| A11Y-LCP | LCP < 1,5 s | **parcial** | Não medido. | Medir primeiro quadro do dashboard (< 1,5 s), por exemplo com `integration_test` + timeline. | +| A11Y-FID | FID < 100 ms | **parcial** | Não medido. | Medir a resposta ao toque no botão de pânico (< 100 ms). | +| A11Y-CLS | CLS < 0,1 | **parcial** | Não medido. | Medir estabilidade do layout no carregamento (< 0,1). | + +## LGPD e dados + +| ID | Requisito | Status | Evidência | O que falta | +|----|-----------|--------|-----------|-------------| +| LGPD-RF01 | Coleta mínima | **parcial** | `spec/lgpd_data_audit.md` classifica campo a campo as 25 tabelas de domínio da migração `20261014155304871`. | `alert_outbox` aparece com o nome do modelo (`alert_outbox_entry`, `spec/lgpd_data_audit.md:94`) e sem os campos `topic`, `lastError` e `attempts`: corrigir nome e campos e conferir a contagem a cada migração. | +| LGPD-RF02 | Consentimento granular | **entregue** | Consentimento por finalidade com histórico (`consent_logs`, `patients.updateConsent`, `hasGrantedConsent`); token de push só com consentimento; testes de endpoint passam na linha de base. | Validação jurídica do texto dos termos (ver M3.4). | +| LGPD-RF07 | Retenção e exclusão (job de expurgo) | **parcial** | Expurgo diário no servidor por `ExpurgeFutureCall` (`backend/sinalacs_server/lib/src/application/retention/expurge_future_call.dart`, agendado em `server.dart:158`): alertas 2 anos, visitas e triagens 5 anos (`RETENTION_*_DAYS`); `expurge_service_test.dart` e `retention_expurge_test.dart` passam (12 testes, 2026-10-15). | Expurgo no aparelho da fila de um dono que nunca volta (ACS); prazos de OTP, convites e tokens não entram na política. | +| LGPD-RF08 | Direitos do titular (Meus Dados + exportação JSON) | **entregue** | Painel Meus Dados (`patients.myData`, `PatientDataOverview`) e exportação em JSON copiada para a área de transferência (`apps/patient/lib/app/app.dart:2040-2084`, botão `Copiar meus dados (JSON)`); pedidos de exclusão/correção atendidos no backoffice (#42). | Validar as regras de anonimização com o encarregado (DPO); exportação como arquivo, não só área de transferência. | +| LGPD-RF09 | Proteção de dados sensíveis | **parcial** | Ver INV-04, CRIPTO-* e RNF06. | Colunas de risco em texto plano em `triage_sessions`, `visits` e `alerts` (INV-04). | +| LGPD-RF11 | Controle de acesso RBAC | **parcial** | Ver RNF06. | Matriz §4.2.2: paciente lendo os próprios logs e justificativa do coordenador. | + +### Modelo de dados e telemetria (§1.3, §3) + +| ID | Requisito | Status | Evidência | O que falta | +|----|-----------|--------|-----------|-------------| +| DADOS-TIMESCALE | Logs e telemetria em TimescaleDB | **ausente** | Nenhuma ocorrência de TimescaleDB no repositório; logs e auditoria em tabelas comuns do Postgres. | Decidir se TimescaleDB entra (não está em `spec/stack.md`) ou corrigir o §3.2 do PRD. | +| DADOS-VERSION-OTIMISTA | Versionamento otimista com 409 | **entregue** | `visits.version`; versão divergente devolve `conflict` com a versão do servidor sem sobrescrever, mesma versão é idempotente (`backend/sinalacs_server/lib/src/application/visits/visit_sync_service.dart:490-500`); `visit_sync_service_test.dart` passa. | Resolução do conflito pelo ACS não existe (ver R1). | +| DADOS-SYNC-TOKEN | sync_token sequencial | **ausente** | Nenhum `sync_token` sequencial; a idempotência é por `localId` + `version` por registro e a descida usa cursor (`visits.pull`). | Decidir se o `sync_token` ainda é necessário ou corrigir o §3.4 do PRD. | +| DADOS-MIGRACAO-ROLLBACK | Migrações com rollback | **ausente** | Migrações do Serverpod só para frente (`backend/sinalacs_server/migrations/*/migration.sql`); nenhum script de rollback. | Rollback por versão ou decisão registrada de restauração por backup. | +| DADOS-CACHE-PACIENTE | Cache local do paciente em `sqflite` com triagens e alertas pendentes (§3.2) | **ausente** | O app do paciente não guarda triagens nem alertas pendentes no aparelho; só lembretes e preferências de consentimento (ver M1.5). | Corrigir o §3.2 do PRD ou decidir guardar o alerta pendente quando não há rede. | +| METRICA-TMRAV | North Star: TMRAV < 90 s | **parcial** | Calculado em `AdminIndicators.tmravSeconds` a partir de `triggeredAt`/`acknowledgedAt`. | Nenhuma medição contra a meta de 90 s (só existe em piloto). | +| METRICA-PRODUTO | Sincronização > 99,5 %, aderência > 90 %, ≥ 8 visitas/ACS/dia, retenção > 70 % (§1.3) | **ausente** | Nenhuma das quatro é calculada ou exibida; sem os eventos do §1.3 (EVT-*). | Telemetria e indicadores no backoffice. | +| EVT-alert_triggered | Evento alert_triggered | **ausente** | Nenhuma emissão do evento em `backend/` ou `apps/*/lib` (busca de 2026-10-15). Os carimbos `triggeredAt`/`acknowledgedAt` em `alerts` permitem o TMRAV (`AdminIndicators.tmravSeconds`). | Telemetria estruturada dos eventos do §1.3. | +| EVT-alert_received_acs | Evento alert_received_acs | **ausente** | Nenhuma emissão do evento. | Emitir com `latency_ms` no ACK. | +| EVT-sync_attempt | Evento sync_attempt | **ausente** | Nenhuma emissão do evento. | Emitir no início de cada lote de sync. | +| EVT-sync_result | Evento sync_result | **ausente** | Nenhuma emissão do evento (a única ocorrência é o tipo `VisitSyncResult`). | Emitir contagens de sucesso/falha/conflito. Sem os eventos do §1.3 a Taxa de Sincronização Offline, o Engajamento do ACS e a Retenção de Pacientes não são mensuráveis em campo. | +| EVT-visit_registered | Evento visit_registered | **ausente** | Nenhuma emissão do evento. | Emitir com `offline_flag` e `sync_status`. | + +## Roadmap, hipóteses e riscos + +| ID | Requisito | Status | Evidência | O que falta | +|----|-----------|--------|-----------|-------------| +| M1.1 | Docker Compose local | **entregue** | `docker-compose.yml` sobe Postgres, Mosquitto, backend, Traefik e Gorush; todos em execução e usados pela medição de latência e pelos scripts TLS/mTLS (2026-10-15). | | +| M1.2 | CI básica | **entregue** | `.github/workflows/ci.yml` com 9 jobs (`flutter test`, `dart analyze`, build Docker, e2e Android), guardados por `scripts/qa/ci_invariants.sh`. | | +| M1.3 | Motor de triagem (MCDC, 50 cenários) | **entregue** | Ver RF16: 64 combinações (tabela-verdade completa) + 8 cenários nomeados. | Sem relatório MCDC por ferramenta; a tabela-verdade completa o torna redundante para seis booleanos. | +| M1.4 | FSM de sincronização | **entregue** | `backend/sinalacs_server/lib/src/application/sync/sync_fsm.dart` e a FSM do aparelho em `offline_visit_queue.dart`; testes de transição, conflito e erro passam na linha de base. | | +| M1.5 | SQLCipher local | **entregue** | SQLCipher no ACS (`apps/acs/lib/core/database/encrypted_database.dart`, `integration_test/encrypted_storage_test.dart`). O paciente usa `sqflite` sem cifra, de propósito, para lembretes e preferências de consentimento. | Lembretes de saúde do paciente em texto livre sem cifra (ver STRIDE-I). | +| M2.1 | App Paciente MVP | **parcial** | Login, alerta, triagem e status no app do paciente (RF01, RF03, RF04, RF05). | SMS real (RF01) e o estado Atendido do status (RF05). | +| M2.2 | App ACS MVP | **entregue** | Login, territorialização, dashboard e registro no ACS (RF07, RF08, RF09, RF11). | | +| M2.3 | MQTT com TLS (WSS) | **parcial** | MQTT sobre TLS com mTLS na 8883 (`mtls_invariants.sh` OK). | O critério fala em WSS, e WebSockets (9001) não é publicado por decisão; o broker aceita TLS 1.2 (ver RNF04). | +| M2.4 | 100 registros sincronizam em < 5 s | **parcial** | 100 visitas por lote em p95 112 ms, 10/10 (meta < 5 s), `measure_latency.dart` contra a stack local (2026-10-16 UTC); `offline_burst_test.dart`. | Só stack local, 1 cliente sequencial, 10 amostras (p95 de 10 amostras é quase o máximo): repetir em produção, com carga e mais amostras. | +| M2.5 | Testes de caos (Toxiproxy) | **parcial** | Simulador em processo `apps/acs/lib/core/services/network_chaos_simulator.dart` + `network_chaos_test.dart`; resiliência real do broker em `scripts/qa/mqtt_resiliencia_e2e.sh`. | Toxiproxy (0 ocorrências): latência, jitter e partição na rede de verdade entre app, Traefik e broker. | +| M2.6 | Testes de usabilidade em campo | **externo** | Avaliação de acessibilidade e e2e no emulador (`spec/ux_accessibility_assessment.md`). | Beta fechado com 5 ACS e 10 pacientes em campo (dono: produto + UBS). | +| M3.1 | Deploy em produção (Pulumi) | **ausente** | Nenhuma ocorrência de Pulumi; `backend/DEPLOY.md` é só um caminho de demo free-tier, não executado. | IaC (Pulumi) e VPS de produção. | +| M3.2 | Monitoramento (OpenTelemetry) | **ausente** | Nenhuma ocorrência de OpenTelemetry, Prometheus ou Grafana. | Traces e métricas em produção. | +| M3.3 | Testes de carga | **ausente** | Nenhum script de carga (100 ACS simultâneos, 500 alertas/hora); `measure_latency.dart` é sequencial, 1 cliente. | Teste de carga. | +| M3.4 | LGPD compliance | **externo** | Documentos de privacidade, painel de consentimento e direitos do titular entregues (LGPD-RF02, LGPD-RF08). | Revisão jurídica do texto real dos termos e validação do DPO (anonimização, exportação da auditoria). | +| M3.5 | Lançamento v1.0 em UBS piloto | **externo** | Nada no código. | Piloto em 1 UBS (2 microáreas, 50 pacientes), depois de M3.1-M3.4. | + +### MVP (§6.1) + +| ID | Requisito | Status | Evidência | O que falta | +|----|-----------|--------|-----------|-------------| +| MVP-LOGIN-60S | 10 pacientes logam em < 60 s | **parcial** | Fluxo OTP funciona (RF01). | SMS real e medição com 10 pacientes. | +| MVP-ALERTA-2S | Alerta chega ao ACS em < 2 s | **parcial** | p95 212 ms na stack local (RNF01). | Só stack local, 1 cliente sequencial, 10 amostras (p95 de 10 amostras é quase o máximo): repetir em produção, com carga e mais amostras. | +| MVP-TRIAGEM-3P | Triagem de 3 perguntas | **entregue** | Seis perguntas, classificação determinística (RF04). | Corrigir 3 para 6 no texto do PRD. | +| MVP-STATUS | Enviado → Visualizado → Atendido | **parcial** | Enviado e Recebido aparecem (RF05). | Atendido nunca aparece (RF05). | +| MVP-LOGIN-ACS | ACS loga com matrícula e senha | **entregue** | Ver RF07. | | +| MVP-TERRITORIO-30S | Microárea carrega em < 30 s | **parcial** | Cache da microárea entregue (RF08). | Medir o download (< 30 s). | +| MVP-FILA-ORDEM | Lista Vermelho → Amarelo → Verde | **entregue** | Ver RF09. | | +| MVP-VISITA-OFFLINE | Visita salva local e sincronizada em background | **parcial** | Gravação local e envio quando a rede volta ou o app abre (RF11). | Envio em segundo plano com o app fechado (WorkManager fora da stack). | +| MVP-MQTT-100 | 100 dispositivos, latência < 500 ms | **parcial** | Latência < 500 ms medida com 1 cliente (RNF01). | 100 dispositivos simultâneos não testados (ver M3.3). | +| MVP-MIGRACOES-LOGS | Migrações versionadas e logs estruturados | **parcial** | Migrações versionadas do Serverpod aplicadas no boot e na CI. | Logs estruturados: o backend usa `session.log` e `stdout.writeln` em texto livre (7 pontos em `lib/src`), sem formato estruturado. | + +### Hipóteses (§1.2) + +| ID | Requisito | Status | Evidência | O que falta | +|----|-----------|--------|-----------|-------------| +| H1 | ACS priorizam mais rápido (−40 %) | **externo** | TMRAV calculado em `AdminIndicators.tmravSeconds`. | Piloto; o início da visita não é ligado ao alerta, então a redução de 40 % só é medível com o evento `visit_registered` (ver EVT-*). | +| H2 | ≥ 80 % completam onboarding sem ajuda | **externo** | Nada instrumentado. | Piloto; registrar conclusão do onboarding sem assistência (evento ausente). | +| H3 | ≥ 90 % de concordância com avaliação médica | **externo** | Nada no código: não existe classificação médica posterior para comparar. | Piloto com avaliação médica e campo para registrá-la (`risk_level_after` existe em `visits`). | +| H4 | ≥ 99,5 % dos registros offline sincronizam | **externo** | Sync medido só em laboratório (RNF02). | Piloto + eventos `sync_attempt`/`sync_result` (ausentes). | + +### Riscos (§6.3) + +| ID | Requisito | Status | Evidência | O que falta | +|----|-----------|--------|-----------|-------------| +| R1 | Colisão de sincronização | **parcial** | Conflito detectado sem sobrescrever (`backend/sinalacs_server/lib/src/application/visits/visit_sync_service.dart:490-500`), mantido em fila própria (`apps/acs/lib/core/services/offline_visit_queue.dart`) e contado na tela (`apps/acs/lib/app/app.dart:3143-3147`, `conflict_visits_count`). | Não existe tela para comparar e escolher versão (UI de resolução lado a lado do plano de contingência): a visita em conflito fica parada na fila. | +| R2 | Falha do MQTT em rede 3G | **parcial** | Reconexão com backoff e sessão persistente no MQTT; outbox no servidor reentrega (`mqtt_resiliencia_e2e.sh`); visitas já vão por HTTP. | Sem reserva por HTTP para o ACS receber alertas quando o MQTT cai (o `backend_client.dart` do ACS não lista alertas pendentes). | +| R3 | Vazamento de PII no dispositivo | **parcial** | Limpeza do aparelho pelo próprio ACS (`apps/acs/lib/app/app.dart:174`, `device_wipe_test.dart`); chave do SQLCipher no Keystore. | Remote wipe pela API (não existe login de supervisor para dispará-lo); rotação de chave. | +| R4 | Rejeição do ACS | **externo** | Nada no código. | NPS semanal e treinamento (produto + UBS). | +| R5 | Performance do PostgreSQL | **parcial** | 46 índices na migração `20261014155304871`, incluindo `alerts_micro_area_status_idx` e `users_micro_area_idx`. | Particionamento por microárea não existe; sem teste de carga que mostre necessidade (M3.3). | +| R6 | Dependência de SMS gateway | **parcial** | O ACS pode gerar quantas vezes quiser um convite QR de uso único para paciente da microárea dele (`backend/sinalacs_server/lib/src/endpoints/onboarding_endpoint.dart:21`), e `completeEnrollment` emite uma sessão do paciente (`onboarding_endpoint.dart:37,70`): é metade do fallback por QR. Nenhum provedor de SMS (RF01). | Entrada sem o ACS presente quando o SMS falha; biometria; múltiplos provedores de SMS. | +| R7 | Mudanças regulatórias (LGPD) | **externo** | Nada no código. | Comitê de compliance e DPO. | +| R8 | SPOF no Traefik | **ausente** | Uma instância de Traefik no Compose. | Segunda instância e failover (infra de produção, M3.1). | + +## Backlog priorizado + +Critério: **P0** toca invariante ou o núcleo do MVP (§6.1) sem critério atingido; **P1** requisito de risco Crítico (§2.2) ou risco de impacto 5 (§6.3); **P2** demais RF/RNF e Fase 3; **P3** fora do MVP e correções do PRD; **—** externo, com dono e sem prioridade de engenharia. + +| # | Prioridade | Lacuna | IDs | Tipo | Justificativa | +|---|---|---|---|---|---| +| 1 | P0 | Colunas de risco da triagem e do alerta em texto plano (`triage_sessions.resultRisk`/`resultDisplay`, `visits.riskLevelBefore`/`riskLevelAfter`, `alerts.riskLevel`) | INV-04, RNF03, LGPD-RF09 | código | Viola uma invariante; ou cifrar, ou registrar no PRD por que o risco precisa ficar legível para a fila. | +| 2 | P0 | Paciente nunca vê Atendido: nenhum caminho leva o alerta a `resolved` | RF05, MVP-STATUS, M2.1 | código | Critério do núcleo do MVP (§6.1) não atingido; precisa da ação do ACS de encerrar o atendimento. | +| 3 | P0 | Envio de OTP por SMS real (provedor + telefone do paciente) | RF01, MVP-LOGIN-60S, M2.1, R6 | código + externo | Sem SMS o paciente só entra com o ACS presente gerando um QR (R6): não há login por conta própria fora do ambiente de dev; a escolha de provedor/contrato é de produto. | +| 4 | P1 | Broker MQTT aceita TLS 1.2; Traefik → backend em HTTP | RNF04, M2.3, CRIPTO-TRAEFIK-BACKEND | infra | Risco Crítico; `tls_version tlsv1.3` no `mosquitto.conf` e o caso no `tls_invariants.sh` é pequeno; mTLS interno exige decisão. | +| 5 | P1 | Resolução de conflito de visita no ACS (comparar e escolher versão) | R1, DADOS-VERSION-OTIMISTA | código | Risco de impacto 5; hoje a visita em conflito fica parada na fila para sempre. | +| 6 | P1 | Reserva por HTTP para o ACS receber alertas quando o MQTT cai | R2, INV-03 | código | Risco de impacto 5 em rede 3G; o ACS não tem como listar alertas pendentes por HTTP. | +| 7 | P1 | Remote wipe e certificado de cliente por aparelho | R3, STRIDE-S, CRIPTO-APP-CACHE | código + infra | Risco de impacto 5; depende de login de supervisor e de PKI de produção. | +| 8 | P1 | Auditoria append-only no banco (trigger/`REVOKE`) | RF17, STRIDE-R | código | A cadeia só detecta; o PRD pede logs imutáveis. | +| 9 | P1 | Telemetria estruturada dos eventos do §1.3 | EVT-*, RNF02, H1, H2, H4, MOD-RELATORIOS | código | Sem os eventos, a North Star e três hipóteses não são mensuráveis no piloto. | +| 10 | P2 | Rate limiting no Mosquitto e no Traefik | STRIDE-D | infra | DoS listado no STRIDE sem mitigação. | +| 11 | P2 | Deploy de produção (Pulumi/VPS), observabilidade e failover do Traefik | M3.1, M3.2, R8 | infra | Fase 3 inteira; pré-requisito do piloto M3.5. | +| 12 | P2 | Teste de carga (100 ACS, 500 alertas/h) e medições de SLO pendentes | M3.3, MVP-MQTT-100, R5, SLO-LOGIN, SLO-DASHBOARD, SLO-VISITA, SLO-TRIAGEM, MVP-TERRITORIO-30S, A11Y-LCP, A11Y-FID, A11Y-CLS | prova | Código existe; falta medir. Paginação de 20 no dashboard junto. | +| 13 | P2 | Testes de caos com Toxiproxy na rede real | M2.5 | prova | O simulador em processo não exercita a rede entre app, Traefik e broker. | +| 14 | P2 | Check-in passivo por geofence nativa | RF12 | código + externo | Bloqueado por revisão de produto/jurídico do texto de divulgação e da localização em segundo plano. | +| 15 | P2 | Clusterização do mapa e chave de Maps de produção | RF10, SLO-MAPA | código | Pedido do §4.1 para 50+ pacientes. | +| 16 | P2 | Push em iOS/APNs, aparelho físico e Gorush hospedado | RF14 | código + infra | Android provado só em emulador. | +| 17 | P2 | Matriz RBAC §4.2.2: paciente lendo os próprios logs; justificativa do coordenador | RNF06, LGPD-RF11 | código | | +| 18 | P2 | Backoffice: cadastro de UBS (com telefone), coordenador pela interface, configurações do sistema | MOD-UBS, MOD-CONFIG, MOD-ACS | código | | +| 19 | P2 | Tema claro no backoffice e foco visível | RF18, A11Y-2.4.7, A11Y-1.4.11 | código | Golden tests para contraste não textual junto. | +| 20 | P2 | Expurgo no aparelho do dono que nunca volta; prazos de OTP/convites/tokens na política | LGPD-RF07 | código | | +| 21 | P2 | `alert_outbox` no inventário LGPD com nome e campos certos | LGPD-RF01 | documentação | Uma tabela; vale prender a contagem num conferidor. | +| 22 | P2 | Logs estruturados no backend | MVP-MIGRACOES-LOGS | código | Hoje `session.log`/`stdout` em texto livre. | +| 23 | P3 | Corrigir o PRD onde o código decidiu outra coisa | RF04 (3→6 perguntas), RF15 (dependência), RF19 (proposto), RNF06 (#40), CRIPTO-APP-LOCAL (PBKDF2), DADOS-TIMESCALE, DADOS-SYNC-TOKEN, DADOS-MIGRACAO-ROLLBACK, STRIDE-T, DADOS-CACHE-PACIENTE | correção do PRD | Texto velho gera auditoria falsa da próxima vez. | +| 24 | P3 | Integração automática com o SAMU | RF13 | fora-do-mvp | v1.5+ no §6.1. | +| 25 | — | Beta em campo, piloto, NPS | M2.6, M3.5, H1, H2, H3, H4, R4 | externo | Dono: produto + UBS. | +| 26 | — | Revisão jurídica dos termos e do DPO (anonimização, exportação da auditoria) | M3.4, LGPD-RF08, R7, RF17 | externo | Dono: jurídico + DPO. | diff --git a/docs/superpowers/plans/2026-10-15-auditoria-lacunas-prd.md b/docs/superpowers/plans/2026-10-15-auditoria-lacunas-prd.md new file mode 100644 index 0000000..d2bbb0c --- /dev/null +++ b/docs/superpowers/plans/2026-10-15-auditoria-lacunas-prd.md @@ -0,0 +1,599 @@ +# Auditoria de lacunas do PRD — Plano de Implementação + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Produzir um relatório verificado, item a item, de tudo que o `spec/PRD_system.md` pede e que ainda não está implementado (ou não está provado), com evidência medida hoje no código, e um backlog priorizado do que falta. + +**Architecture:** Um relatório versionado em `docs/auditoria-prd/2026-10-15-lacunas-prd.md` com uma linha por ID do PRD (66 IDs: RF01–RF19, RNF01–RNF06, INV-01–05, LGPD-RF01/02/07/08/09/11, M1.1–M3.5, H1–H4, R1–R8), mais seções sem ID para SLOs (§4.1), STRIDE (§4.2.1), criptografia (§4.2.3), telemetria (§1.3) e módulos do backoffice (§2.1). Um conferidor determinístico (`scripts/qa/cobertura_prd.py`, no mesmo molde de `scripts/qa/contagem_validation_report.py`) falha se algum ID do PRD ficar sem linha, se houver ID inventado, status desconhecido ou — no modo `--final` — linha sem evidência ou sem "o que falta". As tarefas de verificação preenchem o relatório por grupo de requisitos; a última consolida o backlog. + +**Tech Stack:** Python 3 (`unittest`, só biblioteca padrão) para o conferidor; `grep`/`graphify`, `dart test`, `flutter test` e os scripts de `scripts/qa/` para coletar evidência. + +**Spec:** `spec/PRD_system.md` (fonte). Contexto obrigatório: `PROGRESS.md`, `CLAUDE.md` (raiz), `backend/CLAUDE.md`, `apps/CLAUDE.md`, `spec/lgpd_design.md`, `spec/ux_accessibility_assessment.md`. + +## Global Constraints + +- Idioma: relatório, mensagens do conferidor e commits em português. +- Este plano **não implementa** requisito nenhum: só mede e registra. Corrigir uma lacuna achada vira item do backlog (Task 9), não commit desta branch. +- Evidência é medida **hoje** no código (`caminho:linha`, nome de teste + comando rodado, ou script de `scripts/qa/` com data). `PROGRESS.md` e o próprio PRD servem de hipótese, nunca de prova: `spec/validation_report.md` está desatualizado nos dois sentidos (ver `CLAUDE.md`), e o PRD também tem trechos velhos (ex.: §4.2.2 ainda diz que nenhum endpoint de dados exercita `coordinator`/`admin`, o que a #40 fechou). +- Status permitidos, exatamente estes: `entregue`, `parcial`, `ausente`, `fora-do-mvp`, `externo`, `a-verificar` (este último proibido no modo `--final`). +- Regra de status: + - `entregue` — código existe **e** há teste automatizado ou prova registrada (e2e com data) que exercita o critério de aceite do PRD. + - `parcial` — código existe, mas o critério de aceite do PRD não é atingido ou não está provado (ex.: só Android, só emulador, só stack local, métrica não medida). + - `ausente` — não há código para o requisito. + - `fora-do-mvp` — `ausente` **e** listado em "Fora do Escopo MVP (v1.5+)" do §6.1. Requisito fora do MVP que já tem código recebe `entregue`/`parcial` normalmente. + - `externo` — o lado do código está feito e o que falta depende de terceiro (DPO, jurídico, teste de campo, loja, provisionamento de conta). +- Formato de linha do relatório, sem `|` dentro das células: `| | | **** | | |`. +- Nunca dado real de paciente em evidência, captura ou log (invariante do repo). +- Commits sem atribuição de IA (`CLAUDE.md` do repo prevalece sobre o padrão do harness). +- Depois de mudar código (só o conferidor), rodar `graphify update .`. + +## Review Focus + +- **ID do PRD com sufixo ou negrito diferente** (`| **RF19 (PRD)** *(proposto…)* |`, `| **R1 (Colisão)** |`): o conferidor precisa extrair `RF19` e `R1`, não ignorar a linha — teste `test_extrai_ids_com_sufixo_e_negrito` (Task 1). +- **`LGPD-RF19` citado em prosa** ("não confundir com LGPD-RF19") não pode virar ID obrigatório: só conta ID na primeira célula de uma linha de tabela — teste `test_ignora_id_citado_fora_da_primeira_celula` (Task 1). +- **Mesmo ID em duas tabelas do PRD** (R1–R8 aparecem na matriz de riscos e nos planos de contingência): uma linha no relatório basta; duas linhas no relatório são erro — testes `test_id_repetido_no_prd_conta_uma_vez` e `test_linha_duplicada_no_relatorio_e_erro` (Task 1). +- **Célula com `|` dentro ou coluna faltando** desalinha status e evidência em silêncio: o conferidor sai com 2, não aceita a linha — teste `test_coluna_a_mais_ou_a_menos_e_erro` (Task 1). +- **Status certo pelo motivo errado** — marcar `entregue` porque o `PROGRESS.md` diz: cada task de verificação exige o comando que produziu a evidência no passo "Registrar"; a Task 9 refaz por amostragem três linhas `entregue` escolhidas ao acaso e rebaixa a linha se a evidência não se reproduzir. + +--- + +## Estrutura de arquivos + +| Arquivo | Ação | Responsabilidade | +|---|---|---| +| `scripts/qa/cobertura_prd.py` | Criar | Extrai IDs do PRD, lê as linhas do relatório, aponta lacunas/IDs inventados/linhas malformadas. | +| `scripts/qa/cobertura_prd_test.py` | Criar | Testes herméticos do conferidor (fixtures inline). | +| `docs/auditoria-prd/2026-10-15-lacunas-prd.md` | Criar | O relatório: tabela por grupo + seções sem ID + backlog. | +| `PROGRESS.md` | Modificar (Task 9) | Uma seção curta apontando para o relatório. | + +--- + +### Task 1: Conferidor de cobertura do PRD + +**Files:** +- Create: `scripts/qa/cobertura_prd.py` +- Test: `scripts/qa/cobertura_prd_test.py` + +**Interfaces:** +- Produces: + - `STATUS: list[str]` — os seis status permitidos. + - `ids_do_prd(texto: str) -> set[str]` + - `linhas_do_relatorio(texto: str) -> dict[str, tuple[str, str, str]]` — `id -> (status, evidência, o_que_falta)`; levanta `Malformado`. + - `conferir(prd: str, relatorio: str, final: bool = False) -> list[str]` — lista de erros, vazia se ok. + - CLI: `python3 scripts/qa/cobertura_prd.py [--final]` → exit 0 ok, 1 lacuna, 2 malformado/uso errado. + +- [ ] **Step 1: Escrever os testes que falham** + +`scripts/qa/cobertura_prd_test.py`: + +```python +import unittest + +from cobertura_prd import Malformado, conferir, ids_do_prd, linhas_do_relatorio + +PRD = """\ +| ID | Requisito | +|----|-----------| +| **RF01** | Login | +| **RF19 (PRD)** *(proposto; não confundir com LGPD-RF19)* | Escalonar | +| **INV-01** | Microárea | +| LGPD-RF02 | Consentimento | +| M1.1 | Compose | +| H1 | Hipótese | +| **R1: Colisão** | Risco | +| **R1 (Colisão)** | Contingência | + +Texto corrido citando RF02 e LGPD-RF19 não conta. +""" + +RELATORIO_OK = """\ +| ID | Requisito | Status | Evidência | O que falta | +|----|-----------|--------|-----------|-------------| +| RF01 | Login | **entregue** | `a.dart:1` | | +| RF19 | Escalonar | **parcial** | `b.dart:2` | prova em aparelho | +| INV-01 | Microárea | **entregue** | `c_test.dart` | | +| LGPD-RF02 | Consentimento | **externo** | `d.dart:3` | revisão do DPO | +| M1.1 | Compose | **entregue** | `docker-compose.yml` | | +| H1 | Hipótese | **externo** | — | teste de campo | +| R1 | Colisão | **ausente** | | UI de resolução | +""" + + +class IdsDoPrdTest(unittest.TestCase): + def test_extrai_ids_com_sufixo_e_negrito(self): + self.assertEqual( + ids_do_prd(PRD), + {"RF01", "RF19", "INV-01", "LGPD-RF02", "M1.1", "H1", "R1"}, + ) + + def test_ignora_id_citado_fora_da_primeira_celula(self): + ids = ids_do_prd(PRD) + self.assertNotIn("LGPD-RF19", ids) + self.assertNotIn("RF02", ids) + + def test_id_repetido_no_prd_conta_uma_vez(self): + self.assertEqual(conferir(PRD, RELATORIO_OK), []) + + +class LinhasDoRelatorioTest(unittest.TestCase): + def test_le_status_evidencia_e_falta(self): + linhas = linhas_do_relatorio(RELATORIO_OK) + self.assertEqual(linhas["RF19"], ("parcial", "`b.dart:2`", "prova em aparelho")) + + def test_status_desconhecido_e_erro(self): + with self.assertRaises(Malformado): + linhas_do_relatorio("| RF01 | Login | **pronto** | x | |\n") + + def test_status_sem_negrito_e_erro(self): + with self.assertRaises(Malformado): + linhas_do_relatorio("| RF01 | Login | entregue | x | |\n") + + def test_coluna_a_mais_ou_a_menos_e_erro(self): + with self.assertRaises(Malformado): + linhas_do_relatorio("| RF01 | Login | **entregue** | a | b | c |\n") + with self.assertRaises(Malformado): + linhas_do_relatorio("| RF01 | Login | **entregue** | a |\n") + + def test_linha_duplicada_no_relatorio_e_erro(self): + with self.assertRaises(Malformado): + linhas_do_relatorio(RELATORIO_OK + "| RF01 | Login | **entregue** | x | |\n") + + +class ConferirTest(unittest.TestCase): + def test_id_do_prd_sem_linha_e_lacuna(self): + relatorio = RELATORIO_OK.replace("| H1 | Hipótese | **externo** | — | teste de campo |\n", "") + self.assertEqual(conferir(PRD, relatorio), ["H1: está no PRD e falta no relatório"]) + + def test_id_inventado_e_erro(self): + relatorio = RELATORIO_OK + "| RF20 | Inexistente | **ausente** | | tudo |\n" + self.assertEqual(conferir(PRD, relatorio), ["RF20: está no relatório e não existe no PRD"]) + + def test_a_verificar_passa_sem_final_e_falha_com_final(self): + relatorio = RELATORIO_OK.replace("**entregue** | `a.dart:1`", "**a-verificar** | `a.dart:1`") + self.assertEqual(conferir(PRD, relatorio), []) + self.assertEqual(conferir(PRD, relatorio, final=True), ["RF01: ainda a-verificar"]) + + def test_final_exige_evidencia_e_o_que_falta(self): + relatorio = RELATORIO_OK.replace("**parcial** | `b.dart:2` | prova em aparelho", "**parcial** | | ") + self.assertEqual( + conferir(PRD, relatorio, final=True), + ["RF19: parcial sem evidência", "RF19: parcial sem o que falta"], + ) + + def test_ausente_pode_ficar_sem_evidencia(self): + self.assertEqual(conferir(PRD, RELATORIO_OK, final=True), []) + + +if __name__ == "__main__": + unittest.main() +``` + +- [ ] **Step 2: Rodar e ver falhar** + +Run: `python3 -m unittest discover -s scripts/qa -p 'cobertura_prd_test.py' -v` +Expected: FAIL com `ModuleNotFoundError: No module named 'cobertura_prd'`. + +- [ ] **Step 3: Implementar o conferidor** + +`scripts/qa/cobertura_prd.py`: + +```python +#!/usr/bin/env python3 +"""Confere o relatório de lacunas do PRD contra os IDs de spec/PRD_system.md. + +Todo ID que aparece na primeira célula de uma tabela do PRD (RFnn, RNFnn, +INV-nn, LGPD-RFnn, Mn.n, Hn, Rn) precisa de exatamente uma linha no +relatório. ID citado em prosa não conta: "não confundir com LGPD-RF19" não +cria requisito. Linha malformada (coluna a mais ou a menos, status sem +negrito ou fora da lista, ID duplicado) é exit 2, não linha ignorada: +ignorar tiraria o requisito da conta sem ninguém perceber. + +Com --final, a-verificar é erro, entregue/parcial exigem evidência e +parcial/ausente/externo exigem "o que falta". +""" +import pathlib +import re +import sys + +RELATORIO = "docs/auditoria-prd/2026-10-15-lacunas-prd.md" +STATUS = ["entregue", "parcial", "ausente", "fora-do-mvp", "externo", "a-verificar"] +ID = r"(?:RF\d{2}|RNF\d{2}|INV-\d{2}|LGPD-RF\d{2}|M\d\.\d|H\d|R\d)" +ID_NO_PRD = re.compile(rf"^\| \*{{0,2}}({ID})\b", re.M) +LINHA_RELATORIO = re.compile(rf"^\| ({ID}) \|(.*)$", re.M) +STATUS_NEGRITO = re.compile(r"\*\*([^*]+)\*\*") + + +class Malformado(Exception): + pass + + +def ids_do_prd(texto): + return set(ID_NO_PRD.findall(texto)) + + +def linhas_do_relatorio(texto): + linhas = {} + for m in LINHA_RELATORIO.finditer(texto): + ident = m.group(1) + corpo = m.group(2).rstrip() + if not corpo.endswith("|"): + raise Malformado(f"{ident}: linha não termina em '|'") + celulas = [c.strip() for c in corpo[:-1].split("|")] + if len(celulas) != 4: + raise Malformado(f"{ident}: esperava 5 colunas, achei {len(celulas) + 1}") + _, status, evidencia, falta = celulas + negrito = STATUS_NEGRITO.fullmatch(status) + if not negrito: + raise Malformado(f"{ident}: status sem negrito: {status!r}") + if negrito.group(1) not in STATUS: + raise Malformado(f"{ident}: status desconhecido: {negrito.group(1)!r}") + if ident in linhas: + raise Malformado(f"{ident}: linha duplicada") + linhas[ident] = (negrito.group(1), evidencia, falta) + return linhas + + +def conferir(prd, relatorio, final=False): + ids = ids_do_prd(prd) + linhas = linhas_do_relatorio(relatorio) + erros = [f"{i}: está no PRD e falta no relatório" for i in sorted(ids - linhas.keys())] + erros += [f"{i}: está no relatório e não existe no PRD" for i in sorted(linhas.keys() - ids)] + if final: + for ident, (status, evidencia, falta) in sorted(linhas.items()): + if status == "a-verificar": + erros.append(f"{ident}: ainda a-verificar") + if status in ("entregue", "parcial") and not evidencia: + erros.append(f"{ident}: {status} sem evidência") + if status in ("parcial", "ausente", "externo") and not falta: + erros.append(f"{ident}: {status} sem o que falta") + return erros + + +def main(argv): + final = "--final" in argv + if [a for a in argv if a != "--final"]: + print("uso: cobertura_prd.py [--final]", file=sys.stderr) + return 2 + raiz = pathlib.Path(__file__).resolve().parents[2] + prd = (raiz / "spec/PRD_system.md").read_text(encoding="utf-8") + relatorio = (raiz / RELATORIO).read_text(encoding="utf-8") + try: + erros = conferir(prd, relatorio, final) + except Malformado as erro: + print(f"relatório malformado: {erro}", file=sys.stderr) + return 2 + for erro in erros: + print(erro, file=sys.stderr) + if erros: + return 1 + print(f"cobertura do PRD: {len(ids_do_prd(prd))} IDs, ok") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) +``` + +Note on `test_ausente_pode_ficar_sem_evidencia`: em `RELATORIO_OK`, `R1` é `ausente` sem evidência e com "o que falta", e `H1` é `externo` com evidência `—` — os dois passam no `--final`. + +- [ ] **Step 4: Rodar e ver passar** + +Run: `python3 -m unittest discover -s scripts/qa -p 'cobertura_prd_test.py' -v` +Expected: 13 testes, `OK`. + +- [ ] **Step 5: Conferir a extração contra o PRD real** + +Run: +```bash +python3 - <<'EOF' +import sys; sys.path.insert(0, "scripts/qa") +import pathlib, cobertura_prd as c +ids = c.ids_do_prd(pathlib.Path("spec/PRD_system.md").read_text()) +print(len(ids), sorted(ids)) +EOF +``` +Expected: `66` IDs — H1–H4, INV-01–05, LGPD-RF01/02/07/08/09/11, M1.1–M1.5, M2.1–M2.6, M3.1–M3.5, R1–R8, RF01–RF19, RNF01–RNF06. Se o número for outro, o PRD mudou desde 2026-10-15: registre a diferença no topo do relatório (Task 2) em vez de ajustar a regex para bater. + +- [ ] **Step 6: Commit** + +```bash +chmod +x scripts/qa/cobertura_prd.py +git add scripts/qa/cobertura_prd.py scripts/qa/cobertura_prd_test.py +git commit -m "test(qa): conferidor de cobertura do relatório de lacunas do PRD" +graphify update . +``` + +--- + +### Task 2: Esqueleto do relatório e linha de base dos testes + +**Files:** +- Create: `docs/auditoria-prd/2026-10-15-lacunas-prd.md` + +**Interfaces:** +- Consumes: `python3 scripts/qa/cobertura_prd.py` (Task 1). +- Produces: o relatório com os 66 IDs em `**a-verificar**`, agrupados nas seções que as Tasks 3–8 preenchem: `## Paciente`, `## ACS`, `## Núcleo, auditoria e backoffice`, `## Invariantes e segurança`, `## Não funcionais`, `## LGPD e dados`, `## Roadmap, hipóteses e riscos`, `## Backlog priorizado`. Cada task mexe só na sua seção. + +- [ ] **Step 1: Medir a linha de base dos testes (uma vez, reutilizada pelas tasks seguintes)** + +```bash +docker compose --profile test up -d postgres-test +(cd backend/sinalacs_server && dart test 2>&1 | tail -3) +(cd backend/sinalacs_server && dart test test/unit 2>&1 | tail -3) +for app in patient acs admin; do (cd apps/$app && flutter test 2>&1 | tail -2); done +python3 -m unittest discover -s scripts/qa -p '*_test.py' 2>&1 | tail -3 +git rev-parse --short HEAD +``` + +Anote contagem de testes, falhas e o SHA. Se algum suíte falhar, **não conserte**: registre a falha no topo do relatório; qualquer linha cuja evidência dependa de um teste que falha não pode ser `entregue`. + +- [ ] **Step 2: Escrever o esqueleto** + +Cabeçalho do arquivo: + +```markdown +# Lacunas do PRD — auditoria de 2026-10-15 + +Fonte: `spec/PRD_system.md` no commit ``. Cada linha foi medida no código neste commit; +`PROGRESS.md` serviu de hipótese, não de prova. Conferência: `python3 scripts/qa/cobertura_prd.py --final`. + +**Linha de base dos testes:** backend `` (``), ACS ``, paciente ``, admin ``, scripts/qa ``. + +**Legenda de status:** `entregue` (código + prova do critério do PRD) · `parcial` (código sem o critério provado) +· `ausente` · `fora-do-mvp` (ausente e listado em v1.5+ no §6.1) · `externo` (falta só ação de terceiro). +``` + +Em seguida, uma tabela por seção com o cabeçalho `| ID | Requisito | Status | Evidência | O que falta |` e as linhas abaixo, todas `**a-verificar**` com evidência e "o que falta" vazios: + +- `## Paciente`: RF01, RF02, RF03, RF04, RF05, RF06, RF18 +- `## ACS`: RF07, RF08, RF09, RF10, RF11, RF12, RF13, RF14, RF15 +- `## Núcleo, auditoria e backoffice`: RF16, RF17, RF19, RNF06 + subseção sem ID `### Módulos do backoffice (§2.1)` com linhas `MOD-UBS`, `MOD-ACS`, `MOD-RELATORIOS`, `MOD-AUDITORIA`, `MOD-CONFIG` +- `## Invariantes e segurança`: INV-01 a INV-05 + subseções sem ID `### STRIDE (§4.2.1)` (`STRIDE-S`, `-T`, `-R`, `-I`, `-D`, `-E`) e `### Criptografia (§4.2.3)` (`CRIPTO-APP-LOCAL`, `CRIPTO-APP-CACHE`, `CRIPTO-APP-TRAEFIK`, `CRIPTO-TRAEFIK-BACKEND`, `CRIPTO-POSTGRES`, `CRIPTO-AUDITORIA`) +- `## Não funcionais`: RNF01 a RNF05 + `### SLOs (§4.1)` (`SLO-LOGIN`, `SLO-ALERTA`, `SLO-DASHBOARD`, `SLO-VISITA`, `SLO-SYNC`, `SLO-MAPA`, `SLO-TRIAGEM`) e `### Acessibilidade (§4.3)` (`A11Y-1.4.3`, `A11Y-1.4.11`, `A11Y-2.5.1`, `A11Y-2.5.6`, `A11Y-2.4.7`, `A11Y-3.3.2`, `A11Y-LCP`, `A11Y-FID`, `A11Y-CLS`) +- `## LGPD e dados`: LGPD-RF01, LGPD-RF02, LGPD-RF07, LGPD-RF08, LGPD-RF09, LGPD-RF11 + `### Modelo de dados e telemetria (§1.3, §3)` (`DADOS-TIMESCALE`, `DADOS-VERSION-OTIMISTA`, `DADOS-SYNC-TOKEN`, `DADOS-MIGRACAO-ROLLBACK`, `EVT-alert_triggered`, `EVT-alert_received_acs`, `EVT-sync_attempt`, `EVT-sync_result`, `EVT-visit_registered`) +- `## Roadmap, hipóteses e riscos`: M1.1–M1.5, M2.1–M2.6, M3.1–M3.5, `### MVP (§6.1)` (`MVP-LOGIN-60S`, `MVP-ALERTA-2S`, `MVP-TRIAGEM-3P`, `MVP-STATUS`, `MVP-LOGIN-ACS`, `MVP-TERRITORIO-30S`, `MVP-FILA-ORDEM`, `MVP-VISITA-OFFLINE`, `MVP-MQTT-100`, `MVP-MIGRACOES-LOGS`), H1–H4, R1–R8 +- `## Backlog priorizado`: vazio (Task 9). + +As linhas sem ID do PRD (prefixos `MOD-`, `STRIDE-`, `CRIPTO-`, `SLO-`, `A11Y-`, `DADOS-`, `EVT-`, `MVP-`) não casam a regex do conferidor e por isso não são exigidas por ele — a completude delas é responsabilidade da task dona. + +- [ ] **Step 3: Conferir** + +Run: `python3 scripts/qa/cobertura_prd.py; echo "exit=$?"` +Expected: `cobertura do PRD: 66 IDs, ok`, `exit=0`. +Run: `python3 scripts/qa/cobertura_prd.py --final; echo "exit=$?"` +Expected: 66 linhas `…: ainda a-verificar`, `exit=1` (prova de que o modo final morde). + +- [ ] **Step 4: Commit** + +```bash +git add docs/auditoria-prd/2026-10-15-lacunas-prd.md +git commit -m "docs(auditoria-prd): esqueleto do relatório de lacunas e linha de base dos testes" +``` + +--- + +> **Método comum às Tasks 3–8** (repetido aqui porque cada task pode ser lida isolada): +> 1. Para cada linha, leia o trecho do PRD que define o critério de aceite (a matriz §2.2, o §6.1 para itens do MVP, a tabela própria do item). +> 2. Oriente-se com `graphify query ""` antes de `grep`; depois abra o arquivo e anote `caminho:linha`. +> 3. Ache o teste que exercita o **critério** (não só o código): `grep -rn "" backend/sinalacs_server/test apps/*/test apps/*/integration_test scripts/qa`. +> 4. Rode esse teste isolado (`dart test ` / `flutter test `) e anote `PASS`/`FAIL`. Teste de emulador ou e2e que exige stack não precisa ser rodado de novo se o `PROGRESS.md` registrar a prova com data **e** o código envolvido não mudou desde então (`git log --since= -- ` vazio); anote isso na evidência. +> 5. Aplique a regra de status dos Global Constraints e escreva em "O que falta" uma frase acionável (o que construir ou provar, onde). +> 6. Ao final da seção: `python3 scripts/qa/cobertura_prd.py` (exit 0) e commit só do relatório. + +### Task 3: Verificar o app Paciente (RF01–RF06, RF18) + +**Files:** +- Modify: `docs/auditoria-prd/2026-10-15-lacunas-prd.md` (seção `## Paciente`) + +**Interfaces:** +- Consumes: esqueleto da Task 2. +- Produces: 7 linhas sem `a-verificar`. + +- [ ] **Step 1: RF01 — passwordless (CPF + nascimento + OTP).** Hipóteses: `auth.requestOtp`/`verifyOtp` em `backend/sinalacs_server/lib/src/endpoints/auth_endpoint.dart`; e2e em `scripts/qa/patient_full_e2e.sh`. Critério do PRD: dependência "SMS Gateway" — verifique se existe envio real de SMS ou só relé de desenvolvimento (`scripts/qa/otp_relay.py`): `grep -rni "sms" backend/sinalacs_server/lib | head`. Sem gateway real → `parcial` com "O que falta: integração com gateway de SMS". Critério do MVP "10 pacientes logam em < 60 s" sem medição → `MVP-LOGIN-60S` fica `parcial` (Task 8). +- [ ] **Step 2: RF02 — onboarding por QR.** Hipótese: entregue com câmera (memória de 2026-09-29). Confirme `mobile_scanner` em `apps/patient/pubspec.yaml`, `qr_flutter` em `apps/acs/pubspec.yaml`, endpoint `onboarding_endpoint.dart`, e o teste que consome o convite de uso único. +- [ ] **Step 3: RF03 — botão de urgência (MQTT, GPS).** Confirme publicação via backend (`alerts.createRedAlert`) e o uso de GPS: `grep -rn "geolocator\|Geolocator" apps/patient/lib`. Sem localização no alerta → `parcial`. Botão ≥ 60×60 dp é critério de §4.3 (registre em `A11Y-2.5.6`, Task 6). +- [ ] **Step 4: RF04 — triagem estruturada.** Confirme a árvore de decisão e o "3 perguntas" do §6.1 (`MVP-TRIAGEM-3P`): conte as perguntas no código do formulário e cite o teste. +- [ ] **Step 5: RF05 — painel de status "Enviado → Visualizado → Atendido".** Confirme `alerts.statusFor` e que os três estados aparecem na UI do paciente (`grep -rn "Visualizado\|Atendido" apps/patient/lib`). +- [ ] **Step 6: RF06 — lembretes locais.** Confirme `flutter_local_notifications` e `apps/patient/lib/core/reminders/`. Decisão §2.2.1: local, sem endpoint — ausência de endpoint **não** é lacuna. +- [ ] **Step 7: RF18 — modo escuro nativo.** Para **cada** app: `grep -n "darkTheme\|themeMode" apps/{patient,acs,admin}/lib -r`. Entregue só se os três apps seguem o tema do sistema e há teste (ou captura em `docs/telas-*.md`) do tema escuro; senão `parcial` listando o app que falta. Lembre que contraste em tema escuro precisa dos tokens `*OnSurface` (ver `apps/CLAUDE.md`). +- [ ] **Step 8: Conferir e commit** + +```bash +python3 scripts/qa/cobertura_prd.py +git add docs/auditoria-prd/2026-10-15-lacunas-prd.md +git commit -m "docs(auditoria-prd): verifica requisitos do app paciente" +``` + +--- + +### Task 4: Verificar o app ACS (RF07–RF15) + +**Files:** +- Modify: `docs/auditoria-prd/2026-10-15-lacunas-prd.md` (seção `## ACS`) + +**Interfaces:** +- Consumes: esqueleto da Task 2. +- Produces: 9 linhas sem `a-verificar`. + +- [ ] **Step 1: RF07 — login institucional.** Hipótese: entregue (Argon2id, TOTP, refresh token preso ao Keystore). Evidência: `auth.loginInstitutional` + `scripts/qa/acs_keystore_bound_e2e.sh` (prova só em API 36). Registre em "O que falta" os abertos que o `CLAUDE.md` nomeia e que tocam o login: invalidação da chave por digital nova em API 30+, prova em API 24–29, troca de senha pelo próprio ACS. Esses não derrubam o critério do PRD ("ACS loga com matrícula e senha") — status `entregue` com a nota. +- [ ] **Step 2: RF08 — territorialização.** Confirme o cache da microárea (72 h por dono) e o critério do MVP "< 30 s" (`MVP-TERRITORIO-30S`): procure medição (`grep -rn "30" scripts/qa/measure_latency.dart`); sem medição → `MVP-TERRITORIO-30S` `parcial`. +- [ ] **Step 3: RF09 — fila priorizada.** Confirme ordenação Vermelho → Amarelo → Verde e o teste que a fixa (`grep -rn "sort\|compare" apps/acs/lib/core/services | grep -i risk`). Paginação de 20 itens é SLO (Task 6). +- [ ] **Step 4: RF10 — mapa.** `google_maps_flutter` tem 7 ocorrências no repo. Confirme que é tela usada (não só dependência), a geocélula arredondada da decisão §2.2.1 (§1) e a clusterização acima de 50 pacientes (`SLO-MAPA`). Requer chave de API do Google para rodar em aparelho — se só roda com chave ausente, anote. +- [ ] **Step 5: RF11 — visita offline-first.** Confirme `apps/acs/lib/core/services/offline_visit_queue.dart` (schema v7) e o teste de fila. Registre como "O que falta" o upload em segundo plano pelo SO (WorkManager fora de `spec/stack.md`) e o desbloqueio de fila com token de 7 dias expirado. +- [ ] **Step 6: RF12 — geofencing (check-in passivo).** `geofenc` tem 16 ocorrências. Confirme o geofence atrelado à visita ativa (§2.2.1 §4) e `scripts/qa/acs_gps_e2e.sh`. PRD marca "submissão à loja pendente" → se o código está pronto, status `externo`. +- [ ] **Step 7: RF13 — escalonar para SAMU/UBS.** Confirme `url_launcher`, botão do SAMU (192) e botão da UBS; o PRD fora do MVP fala em "integração SAMU (escalonamento automático)" — se só há discagem manual, `entregue` para RF13 e anote a integração automática como `fora-do-mvp` no backlog. +- [ ] **Step 8: RF14 — avisos push.** Hipótese: `parcial` (Android/FCM provado em emulador 2026-09-30; faltam iOS/APNs, aparelho físico e Gorush hospedado). Confirme `notices_endpoint.dart`, `scripts/qa/push_e2e.sh`, `ci_push_e2e.sh`. +- [ ] **Step 9: RF15 — sync bidirecional.** Confirme `visits.sync`/`pull` por cursor e a FSM (`backend/sinalacs_server/lib/src/**/sync_fsm.dart`). A coluna "Dependência" do PRD diz "ainda não conectado ao cliente" — verifique se isso está velho (o consumo no ACS e no paciente foi feito em 2026-09-18) e, se estiver, anote em "O que falta" a correção da linha do PRD. +- [ ] **Step 10: Conferir e commit** + +```bash +python3 scripts/qa/cobertura_prd.py +git add docs/auditoria-prd/2026-10-15-lacunas-prd.md +git commit -m "docs(auditoria-prd): verifica requisitos do app ACS" +``` + +--- + +### Task 5: Núcleo, auditoria e backoffice (RF16, RF17, RF19, RNF06, módulos §2.1) + +**Files:** +- Modify: `docs/auditoria-prd/2026-10-15-lacunas-prd.md` (seção `## Núcleo, auditoria e backoffice`) + +**Interfaces:** +- Consumes: esqueleto da Task 2. +- Produces: 4 linhas com ID + 5 linhas `MOD-*`. + +- [ ] **Step 1: RF16 — motor de triagem determinístico.** Critério M1.3: "cobertura 100% MCDC; classifica 50 cenários". Ache o motor (`graphify query "triage engine"`) e conte os cenários do teste: `grep -c "test(" `. Rode `./scripts/qa/measure_coverage.sh` (ou o comando de cobertura do `backend/CLAUDE.md`) para o arquivo do motor. MCDC não é medido por ferramenta Dart padrão: se só há cobertura de linha, RF16 é `entregue` (determinismo provado) e M1.3 é `parcial` com "O que falta: medição MCDC". +- [ ] **Step 2: RF17 — auditoria.** Hipótese: `parcial` — cadeia HMAC detecta adulteração mas não impede (sem trigger/`REVOKE` append-only), exportação CSV vai pela área de transferência. Confirme `audit_chain_check` e `scripts/qa/admin_auditoria_e2e.sh`. +- [ ] **Step 3: RF19 — escalonamento de alerta vermelho.** Hipótese: `entregue` (#44, e2e 2026-10-12). Confirme `admin.escalateAlert`, `RED_ALERT_ESCALATION_SECONDS` com faixa 30–3600 s e teste de idempotência. +- [ ] **Step 4: RNF06 — RBAC.** Confirme `Authorization.require` e os testes de negação por papel/UBS nos endpoints `admin`. O texto do PRD ("RBAC dos endpoints de dados do staff segue aberto, issue #40") provavelmente está velho; se estiver, anote "corrigir RNF06 no PRD". Regra de matriz §4.2.2 a verificar linha a linha: "Dados de outras microáreas — Coordenador R (justificado)" — existe registro de justificativa? Se não, `parcial`. +- [ ] **Step 5: Módulos do backoffice (§2.1).** `MOD-UBS` (cadastro de UBS, incluindo telefone — aberto conhecido), `MOD-ACS` (#43), `MOD-RELATORIOS` (Indicadores), `MOD-AUDITORIA` (#45), `MOD-CONFIG` ("Configurações de sistema R/W" do admin, §4.2.2). Para cada um: `ls apps/admin/lib -R | grep -i ` e o endpoint em `admin_endpoint.dart`. Anote também o aberto do `CLAUDE.md`: criar coordenador e definir `ubsId` pela UI. +- [ ] **Step 6: Conferir e commit** + +```bash +python3 scripts/qa/cobertura_prd.py +git add docs/auditoria-prd/2026-10-15-lacunas-prd.md +git commit -m "docs(auditoria-prd): verifica núcleo, auditoria e backoffice" +``` + +--- + +### Task 6: Invariantes, segurança e não funcionais (INV-01–05, RNF01–RNF05, STRIDE, criptografia, SLOs, acessibilidade) + +**Files:** +- Modify: `docs/auditoria-prd/2026-10-15-lacunas-prd.md` (seções `## Invariantes e segurança` e `## Não funcionais`) + +**Interfaces:** +- Consumes: esqueleto da Task 2; linha de base dos testes da Task 2. +- Produces: 10 linhas com ID + linhas `STRIDE-*`, `CRIPTO-*`, `SLO-*`, `A11Y-*`. + +- [ ] **Step 1: INV-01 e INV-05 (segregação).** Para cada endpoint em `backend/sinalacs_server/lib/src/endpoints/*.dart` que devolve dado de paciente, ache o teste que tenta ler fora da microárea (INV-01) e de outro paciente (INV-05) e espera recusa. Liste endpoint por endpoint na evidência; um endpoint sem teste de recusa → `parcial` nomeando o endpoint. +- [ ] **Step 2: INV-02, INV-03, INV-04.** INV-02: confirme que nenhum endpoint/tela aceita risco vindo do cliente (`grep -rn "riskLevel" backend/sinalacs_server/lib/src/endpoints`). INV-03: ACK + retry + outbox; cite `scripts/qa/mqtt_resiliencia_e2e.sh`. INV-04: colunas `*Encrypted` no Postgres e SQLCipher nos apps; procure coluna clínica ainda em texto plano no `definition.sql` da última migração (`ls -d backend/sinalacs_server/migrations/*/ | tail -1`) cruzando com `spec/lgpd_data_audit.md`. +- [ ] **Step 3: STRIDE.** Uma linha por letra. Pontos de atenção que o PRD pede e podem faltar: S — certificado de cliente por aparelho em produção (só stack local hoje); T — "assinatura digital dos dados de triagem no dispositivo"; R — "logs append-only" (só detecção); D — rate limiting no Mosquitto (`grep -n "max_\|rate" infra/docker/mosquitto/*`). +- [ ] **Step 4: Criptografia (§4.2.3).** Compare a tabela do PRD com o real: chave do SQLCipher "derivada do PIN/biometria (PBKDF2)" vs o que `apps/*/lib/core/database/encrypted_database.dart` faz; "chave rotativa"; TLS 1.3 em Traefik (`scripts/qa/tls_invariants.sh`); mTLS Traefik↔backend; "pgcrypto + Vault" vs AES-256-GCM na aplicação (decisão §2.2.1 §6 — divergência decidida não é lacuna, anote como tal). Rode `./scripts/qa/tls_invariants.sh` e `./scripts/qa/mtls_invariants.sh` com a stack de pé se ainda não houver prova datada. +- [ ] **Step 5: RNF01–RNF04.** RNF01 (latência MQTT < 500 ms p95) e `SLO-ALERTA`: rode ou localize resultado de `dart run scripts/qa/measure_latency.dart` — sem p95 medido → `parcial`. RNF02 (> 99,5 % de sync): procure medição; sem métrica coletada → `parcial` ("falta telemetria `sync_result`"). RNF03/RNF04: derivam dos passos 2 e 4. +- [ ] **Step 6: SLOs (§4.1).** Para cada `SLO-*`, registre "medido (valor, comando)" ou "não medido". Não medido é `parcial`, nunca `entregue`. +- [ ] **Step 7: RNF05 e acessibilidade (§4.3).** Fonte: `spec/ux_accessibility_assessment.md` e `apps/*/test/contrast_tokens_test.dart`. Rode `flutter test test/contrast_tokens_test.dart` em cada app. Atenção: o PRD pede WCAG 2.1 AA; a avaliação usa 2.2 AA (superconjunto — ok). Admin (`apps/admin`) tem teste de contraste? Se não, `parcial`. `A11Y-LCP/FID/CLS` sem medição → `parcial`. `A11Y-2.4.7` (foco visível com `FocusNode`) é mais relevante no admin web/desktop. +- [ ] **Step 8: Conferir e commit** + +```bash +python3 scripts/qa/cobertura_prd.py +git add docs/auditoria-prd/2026-10-15-lacunas-prd.md +git commit -m "docs(auditoria-prd): verifica invariantes, segurança e não funcionais" +``` + +--- + +### Task 7: LGPD, modelo de dados e telemetria + +**Files:** +- Modify: `docs/auditoria-prd/2026-10-15-lacunas-prd.md` (seção `## LGPD e dados`) + +**Interfaces:** +- Consumes: esqueleto da Task 2. +- Produces: 6 linhas com ID + linhas `DADOS-*`, `EVT-*`. + +- [ ] **Step 1: LGPD-RF01, 02, 09, 11.** Coleta mínima: existe o "checklist de dados por funcionalidade"? (`spec/lgpd_data_audit.md` conta como tal se cobre todas as tabelas da última migração — conte as tabelas no `definition.sql` e compare.) Consentimento granular: `patients.updateConsent`, `consent_logs`. 09 e 11 derivam de INV-04 e RNF06. +- [ ] **Step 2: LGPD-RF07 — retenção e "job de expurgo".** `grep -rni "expurgo\|purge\|retention\|retencao" backend/sinalacs_server/lib`. O `CLAUDE.md` lista como aberto o expurgo de dono que nunca volta (fila do ACS). Sem job periódico de expurgo no servidor → `ausente` ou `parcial`. +- [ ] **Step 3: LGPD-RF08 — "Meus Dados" + exportação JSON.** Confirme `patients.myData` e o painel; procure a **exportação em JSON** pelo titular (`grep -rni "json" apps/patient/lib | grep -i "export\|baixar\|compartilh"`). Sem exportação → `parcial`. Atendimento de pedidos (#42) entregue; validação das regras de anonimização com o DPO é `externo` no backlog. +- [ ] **Step 4: Modelo de dados (§3) e telemetria (§1.3).** `DADOS-TIMESCALE` (0 ocorrências de `timescale` no repo em 2026-10-15 → provável `ausente`); `DADOS-VERSION-OTIMISTA` (campo `version` e 409 em `visits`); `DADOS-SYNC-TOKEN` ("servidor rejeita tokens menores que o último processado"); `DADOS-MIGRACAO-ROLLBACK` (o PRD pede `rollback/*.sql`; o Serverpod gera migrações — veja se há rollback). Cada `EVT-*`: `grep -rn "" backend apps` — evento não emitido → `ausente`; sem eles, as métricas North Star (§1.3) não são mensuráveis, anote isso uma vez em `EVT-sync_result`. +- [ ] **Step 5: Conferir e commit** + +```bash +python3 scripts/qa/cobertura_prd.py +git add docs/auditoria-prd/2026-10-15-lacunas-prd.md +git commit -m "docs(auditoria-prd): verifica LGPD, modelo de dados e telemetria" +``` + +--- + +### Task 8: Roadmap, MVP, hipóteses e riscos (M1.1–M3.5, MVP-*, H1–H4, R1–R8) + +**Files:** +- Modify: `docs/auditoria-prd/2026-10-15-lacunas-prd.md` (seção `## Roadmap, hipóteses e riscos`) + +**Interfaces:** +- Consumes: linhas já preenchidas pelas Tasks 3–7 (cite-as por ID em vez de repetir a evidência). +- Produces: 34 linhas com ID + 10 linhas `MVP-*`. + +- [ ] **Step 1: Fase 1 e 2 (M1.x, M2.x).** O `PROGRESS.md` marca todos como implementados; confira o critério de aceite de cada um contra o PRD, não o rótulo. Pontos que provavelmente não batem: M1.3 (MCDC, ver Task 5), M2.3 ("WSS" — o broker só expõe MQTT/TLS em 8883; WebSockets 9001 não é publicado: decisão consciente → anote), M2.4 ("100 registros sincronizam em < 5 s" — há medição?), M2.5 ("Toxiproxy" — 0 ocorrências; existe `network_chaos_simulator.dart` em processo, não proxy de rede → `parcial`), M2.6 ("5 ACS e 10 pacientes testam em campo" → `externo`). +- [ ] **Step 2: Fase 3 (M3.x).** M3.1 Pulumi/VPS (0 ocorrências de `pulumi`; `backend/DEPLOY.md` é só demo free-tier → `ausente`). M3.2 OpenTelemetry/Prometheus/Grafana (0 ocorrências → `ausente`). M3.3 carga 100 ACS / 500 alertas/h (procure script de carga → provável `ausente`). M3.4 LGPD (documentação existe; revisão do DPO → `externo`). M3.5 piloto em UBS → `externo`. +- [ ] **Step 3: MVP (§6.1).** Cada `MVP-*` aponta para a linha RF correspondente e diz se o **número** do critério (60 s, 2 s, 30 s, 100 dispositivos, < 500 ms) foi medido. `MVP-MIGRACOES-LOGS`: migrações versionadas sim; "logs estruturados" — procure logger JSON no backend. +- [ ] **Step 4: Hipóteses H1–H4.** Todas dependem de piloto/campo; status `externo`, mas registre em "O que falta" se a **instrumentação** para medi-las existe (H1 precisa de `alert_received_acs` e início de visita; H4 de `sync_result`) — sem instrumentação, a hipótese é inmedível mesmo com piloto, e isso vira item de código no backlog. +- [ ] **Step 5: Riscos R1–R8.** Status do **plano de contingência** em código: R1 (UI de resolução de conflito lado a lado — procure no ACS), R2 (fallback REST quando MQTT cai — `POST /v1/alerts/red`?), R3 (remote wipe via API — o `CLAUDE.md` diz que não há wipe forçado pelo supervisor → `ausente`), R5 (índices/particionamento por microárea), R6 (fallback do SMS), R8 (failover do Traefik → infra, `ausente`). R4 e R7 são de processo → `externo`. +- [ ] **Step 6: Conferir e commit** + +```bash +python3 scripts/qa/cobertura_prd.py +git add docs/auditoria-prd/2026-10-15-lacunas-prd.md +git commit -m "docs(auditoria-prd): verifica roadmap, MVP, hipóteses e riscos" +``` + +--- + +### Task 9: Amostragem, backlog priorizado e fechamento + +**Files:** +- Modify: `docs/auditoria-prd/2026-10-15-lacunas-prd.md` (seção `## Backlog priorizado` e linhas rebaixadas) +- Modify: `PROGRESS.md` (seção nova ao final) + +**Interfaces:** +- Consumes: relatório completo das Tasks 3–8; `python3 scripts/qa/cobertura_prd.py --final`. + +- [ ] **Step 1: Amostragem das linhas `entregue`** + +```bash +grep -E '^\| (RF|RNF|INV|LGPD|M[0-9])[^|]* \|[^|]*\| \*\*entregue\*\*' docs/auditoria-prd/2026-10-15-lacunas-prd.md | shuf -n 3 +``` + +Para cada uma das três, refaça a evidência do zero (abra o `caminho:linha`, rode o teste citado). Se não se reproduzir, rebaixe para `parcial` e registre no topo do relatório "amostragem: X de 3 reproduzidas". + +- [ ] **Step 2: Escrever o backlog priorizado** + +Na seção `## Backlog priorizado`, uma tabela `| # | Lacuna | IDs | Tipo | Prioridade | Justificativa |`, uma linha por lacuna (agrupe IDs que se resolvem com o mesmo trabalho, ex.: `EVT-*` + RNF02 + H4). Tipo: `código`, `prova` (código existe, falta medição/e2e), `infra`, `externo`, `correção do PRD`. Prioridade, nesta ordem de critério: + 1. **P0** — toca invariante (INV-01..05) ou item do núcleo do MVP (§6.1) sem critério atingido. + 2. **P1** — requisito com risco **Crítico** na matriz §2.2 (RF03, RF11, RF15, RF16, RF19, RNF02–RNF04) ou risco de impacto 5 (R1, R2, R3, R7, R8). + 3. **P2** — demais RF/RNF e itens da Fase 3. + 4. **P3** — `fora-do-mvp`, correções de texto do PRD. + Itens `externo` entram com o dono (DPO, jurídico, UBS piloto, loja) e sem prioridade de engenharia. + +- [ ] **Step 3: Conferência final** + +Run: `python3 scripts/qa/cobertura_prd.py --final; echo "exit=$?"` +Expected: `cobertura do PRD: 66 IDs, ok`, `exit=0`. +Run também: `grep -c 'a-verificar' docs/auditoria-prd/2026-10-15-lacunas-prd.md` → `0` (pega as linhas sem ID do PRD, que o conferidor não vê). +Run: `./scripts/qa/check_documentation_links.sh` → sem link quebrado. + +- [ ] **Step 4: Apontar no `PROGRESS.md`** + +Acrescente ao final: + +```markdown +## Auditoria de lacunas do PRD (2026-10-15) + +Relatório item a item de `spec/PRD_system.md` contra o código, com evidência medida no commit ``: +[docs/auditoria-prd/2026-10-15-lacunas-prd.md](docs/auditoria-prd/2026-10-15-lacunas-prd.md). +Contagem: `` entregue, `` parcial, `` ausente, `` fora-do-mvp, `` externo. +Backlog priorizado na seção final do relatório; conferência por `python3 scripts/qa/cobertura_prd.py --final`. +``` + +Gere a contagem com: + +```bash +grep -oE '\*\*(entregue|parcial|ausente|fora-do-mvp|externo)\*\*' docs/auditoria-prd/2026-10-15-lacunas-prd.md | sort | uniq -c +``` + +- [ ] **Step 5: Commit** + +```bash +git add docs/auditoria-prd/2026-10-15-lacunas-prd.md PROGRESS.md +git commit -m "docs(auditoria-prd): backlog priorizado e fechamento da auditoria do PRD" +``` + +- [ ] **Step 6: (Opcional, só com aprovação do dono) abrir issues** + +Para cada item P0/P1 do backlog, **mostre a lista ao dono antes** e só então: + +```bash +gh issue create --title "" --body "IDs do PRD: . Evidência e o que falta: docs/auditoria-prd/2026-10-15-lacunas-prd.md (linha )." +``` diff --git a/scripts/qa/cobertura_prd.py b/scripts/qa/cobertura_prd.py new file mode 100755 index 0000000..1280b5f --- /dev/null +++ b/scripts/qa/cobertura_prd.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +"""Confere o relatório de lacunas do PRD contra os IDs de spec/PRD_system.md. + +Todo ID que aparece na primeira célula de uma tabela do PRD (RFnn, RNFnn, +INV-nn, LGPD-RFnn, Mn.n, Hn, Rn) precisa de exatamente uma linha no +relatório. ID citado em prosa não conta: "não confundir com LGPD-RF19" não +cria requisito. Linha malformada (coluna a mais ou a menos, status sem +negrito ou fora da lista, ID duplicado) é exit 2, não linha ignorada: +ignorar tiraria o requisito da conta sem ninguém perceber. + +Com --final, a-verificar é erro, entregue/parcial exigem evidência e +parcial/ausente/externo exigem "o que falta". +""" +import pathlib +import re +import sys + +RELATORIO = "docs/auditoria-prd/2026-10-15-lacunas-prd.md" +STATUS = ["entregue", "parcial", "ausente", "fora-do-mvp", "externo", "a-verificar"] +ID = r"(?:RF\d{2}|RNF\d{2}|INV-\d{2}|LGPD-RF\d{2}|M\d\.\d|H\d|R\d)" +ID_NO_PRD = re.compile(rf"^\| \*{{0,2}}({ID})\b", re.M) +LINHA_RELATORIO = re.compile(rf"^\| ({ID}) \|(.*)$", re.M) +CELULA_DE_ID = re.compile(rf"^\|\s*\**\s*({ID})\s*\**\s*\|", re.M) +STATUS_NEGRITO = re.compile(r"\*\*([^*]+)\*\*") + + +class Malformado(Exception): + pass + + +def ids_do_prd(texto): + return set(ID_NO_PRD.findall(texto)) + + +def linhas_do_relatorio(texto): + # Célula de ID fora do formato `| ID |` (espaço a mais, negrito, sem + # espaço) escaparia de LINHA_RELATORIO e sumiria da conta — inclusive como + # duplicata de uma linha válida. + for m in CELULA_DE_ID.finditer(texto): + if not LINHA_RELATORIO.match(texto, m.start()): + raise Malformado(f"{m.group(1)}: célula de ID fora do formato '| ID |'") + linhas = {} + for m in LINHA_RELATORIO.finditer(texto): + ident = m.group(1) + corpo = m.group(2).rstrip() + if not corpo.endswith("|"): + raise Malformado(f"{ident}: linha não termina em '|'") + celulas = [c.strip() for c in corpo[:-1].split("|")] + if len(celulas) != 4: + raise Malformado(f"{ident}: esperava 5 colunas, achei {len(celulas) + 1}") + _, status, evidencia, falta = celulas + negrito = STATUS_NEGRITO.fullmatch(status) + if not negrito: + raise Malformado(f"{ident}: status sem negrito: {status!r}") + if negrito.group(1) not in STATUS: + raise Malformado(f"{ident}: status desconhecido: {negrito.group(1)!r}") + if ident in linhas: + raise Malformado(f"{ident}: linha duplicada") + linhas[ident] = (negrito.group(1), evidencia, falta) + return linhas + + +def conferir(prd, relatorio, final=False): + ids = ids_do_prd(prd) + linhas = linhas_do_relatorio(relatorio) + erros = [f"{i}: está no PRD e falta no relatório" for i in sorted(ids - linhas.keys())] + erros += [f"{i}: está no relatório e não existe no PRD" for i in sorted(linhas.keys() - ids)] + if final: + for ident, (status, evidencia, falta) in sorted(linhas.items()): + if status == "a-verificar": + erros.append(f"{ident}: ainda a-verificar") + if status in ("entregue", "parcial") and not evidencia: + erros.append(f"{ident}: {status} sem evidência") + if status in ("parcial", "ausente", "externo") and not falta: + erros.append(f"{ident}: {status} sem o que falta") + return erros + + +def main(argv): + final = "--final" in argv + if [a for a in argv if a != "--final"]: + print("uso: cobertura_prd.py [--final]", file=sys.stderr) + return 2 + raiz = pathlib.Path(__file__).resolve().parents[2] + prd = (raiz / "spec/PRD_system.md").read_text(encoding="utf-8") + relatorio = (raiz / RELATORIO).read_text(encoding="utf-8") + try: + erros = conferir(prd, relatorio, final) + except Malformado as erro: + print(f"relatório malformado: {erro}", file=sys.stderr) + return 2 + for erro in erros: + print(erro, file=sys.stderr) + if erros: + return 1 + print(f"cobertura do PRD: {len(ids_do_prd(prd))} IDs, ok") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/scripts/qa/cobertura_prd_test.py b/scripts/qa/cobertura_prd_test.py new file mode 100644 index 0000000..914e14d --- /dev/null +++ b/scripts/qa/cobertura_prd_test.py @@ -0,0 +1,108 @@ +import unittest + +from cobertura_prd import Malformado, conferir, ids_do_prd, linhas_do_relatorio + +PRD = """\ +| ID | Requisito | +|----|-----------| +| **RF01** | Login | +| **RF19 (PRD)** *(proposto; não confundir com LGPD-RF19)* | Escalonar | +| **INV-01** | Microárea | +| LGPD-RF02 | Consentimento | +| M1.1 | Compose | +| H1 | Hipótese | +| **R1: Colisão** | Risco | +| **R1 (Colisão)** | Contingência | + +Texto corrido citando RF02 e LGPD-RF19 não conta. +""" + +RELATORIO_OK = """\ +| ID | Requisito | Status | Evidência | O que falta | +|----|-----------|--------|-----------|-------------| +| RF01 | Login | **entregue** | `a.dart:1` | | +| RF19 | Escalonar | **parcial** | `b.dart:2` | prova em aparelho | +| INV-01 | Microárea | **entregue** | `c_test.dart` | | +| LGPD-RF02 | Consentimento | **externo** | `d.dart:3` | revisão do DPO | +| M1.1 | Compose | **entregue** | `docker-compose.yml` | | +| H1 | Hipótese | **externo** | — | teste de campo | +| R1 | Colisão | **ausente** | | UI de resolução | +""" + + +class IdsDoPrdTest(unittest.TestCase): + def test_extrai_ids_com_sufixo_e_negrito(self): + self.assertEqual( + ids_do_prd(PRD), + {"RF01", "RF19", "INV-01", "LGPD-RF02", "M1.1", "H1", "R1"}, + ) + + def test_ignora_id_citado_fora_da_primeira_celula(self): + ids = ids_do_prd(PRD) + self.assertNotIn("LGPD-RF19", ids) + self.assertNotIn("RF02", ids) + + def test_id_repetido_no_prd_conta_uma_vez(self): + self.assertEqual(conferir(PRD, RELATORIO_OK), []) + + +class LinhasDoRelatorioTest(unittest.TestCase): + def test_le_status_evidencia_e_falta(self): + linhas = linhas_do_relatorio(RELATORIO_OK) + self.assertEqual(linhas["RF19"], ("parcial", "`b.dart:2`", "prova em aparelho")) + + def test_status_desconhecido_e_erro(self): + with self.assertRaises(Malformado): + linhas_do_relatorio("| RF01 | Login | **pronto** | x | |\n") + + def test_status_sem_negrito_e_erro(self): + with self.assertRaises(Malformado): + linhas_do_relatorio("| RF01 | Login | entregue | x | |\n") + + def test_coluna_a_mais_ou_a_menos_e_erro(self): + with self.assertRaises(Malformado): + linhas_do_relatorio("| RF01 | Login | **entregue** | a | b | c |\n") + with self.assertRaises(Malformado): + linhas_do_relatorio("| RF01 | Login | **entregue** | a |\n") + + def test_celula_de_id_fora_do_formato_e_erro(self): + for linha in ( + "| RF01 | Login | **ausente** | | x |\n", + "|RF01| Login | **ausente** | | x |\n", + "| **RF01** | Login | **ausente** | | x |\n", + ): + with self.subTest(linha=linha), self.assertRaises(Malformado): + linhas_do_relatorio(RELATORIO_OK + linha) + + def test_linha_duplicada_no_relatorio_e_erro(self): + with self.assertRaises(Malformado): + linhas_do_relatorio(RELATORIO_OK + "| RF01 | Login | **entregue** | x | |\n") + + +class ConferirTest(unittest.TestCase): + def test_id_do_prd_sem_linha_e_lacuna(self): + relatorio = RELATORIO_OK.replace("| H1 | Hipótese | **externo** | — | teste de campo |\n", "") + self.assertEqual(conferir(PRD, relatorio), ["H1: está no PRD e falta no relatório"]) + + def test_id_inventado_e_erro(self): + relatorio = RELATORIO_OK + "| RF20 | Inexistente | **ausente** | | tudo |\n" + self.assertEqual(conferir(PRD, relatorio), ["RF20: está no relatório e não existe no PRD"]) + + def test_a_verificar_passa_sem_final_e_falha_com_final(self): + relatorio = RELATORIO_OK.replace("**entregue** | `a.dart:1`", "**a-verificar** | `a.dart:1`") + self.assertEqual(conferir(PRD, relatorio), []) + self.assertEqual(conferir(PRD, relatorio, final=True), ["RF01: ainda a-verificar"]) + + def test_final_exige_evidencia_e_o_que_falta(self): + relatorio = RELATORIO_OK.replace("**parcial** | `b.dart:2` | prova em aparelho", "**parcial** | | ") + self.assertEqual( + conferir(PRD, relatorio, final=True), + ["RF19: parcial sem evidência", "RF19: parcial sem o que falta"], + ) + + def test_ausente_pode_ficar_sem_evidencia(self): + self.assertEqual(conferir(PRD, RELATORIO_OK, final=True), []) + + +if __name__ == "__main__": + unittest.main()