Fatia (d) de #106, e a única que não corrige um site — cria o gate que impede a recaída.
⛔ Bloqueada por #126, #127, #128 e #129. Este gate afirma "zero botões só de ícone sem nome" sobre components/ e app/ inteiros. Enquanto qualquer um dos 9 sites da #106 estiver sem rótulo, ele é vermelho por construção — mergear antes deixa o main com CI quebrado. Não aplicar claude-ready aqui até as quatro fatias de correção terem mergeado.
(A label claude-bloqueada prevista em .claude/routines.md não existe no repo; sem ela, o bloqueio está registrado no título e aqui. Vale criar a label — claude-precisa-fatiar existe, claude-bloqueada não.)
A alternativa — mergear o scanner primeiro com uma allowlist dos sites pendentes, que encolhe a cada fatia — foi descartada de propósito: allowlist é exatamente o mecanismo pelo qual um gate apodrece, e cria acoplamento entre PRs que hoje são independentes.
O problema que este gate resolve
As fatias #126–#129 corrigem os 9 sites conhecidos e cada uma traz uma asserção ancorada no seu próprio site (recorte <Button ... <Icone, no padrão de row-actions.test.ts e a11y-estado-selecao.test.ts). Essas asserções protegem contra regressão nos 9 sites, e só neles.
Nada impede o 10º: um <Button size="icon"> novo, em arquivo novo, sem rótulo. O lint não pega — eslint.config.mjs:25-32 escopa jsx-a11y/label-has-associated-control a components/ui/field.tsx e nada mais, e fora disso vale só o subconjunto do eslint-config-next/core-web-vitals, que não tem regra de nome acessível. O CI roda npm run lint --max-warnings 0 em todo push e ficou verde durante os meses em que os 9 sites existiram — essa é a evidência direta de que o gate atual não cobre a propriedade.
O padrão se repetiu 9 vezes em 8 arquivos sem ninguém notar. É o critério para valer um gate estrutural em vez de mais uma asserção por arquivo.
Proposta
Estender __tests__/unit/a11y-nomes-acessiveis.test.ts (criado pela #126) com uma varredura de components/**/*.tsx e app/**/*.tsx: para cada <button>/<Button>, se o corpo contiver apenas componentes de ícone auto-fechados (sem texto, sem {expressão} que renderize texto), exigir aria-label, aria-labelledby, title ou um filho sr-only.
expect(botoesSemNome).toEqual([])
A mensagem de falha deve listar arquivo:linha de cada infrator — um toEqual([]) sobre array de strings já entrega isso no diff do Vitest, sem formatação extra.
Armadilha de implementação que já custou uma rodada
A regex óbvia para a tag de abertura, <(button|Button)\b([^>]*?)>, não encontra o site 9 da #106 (SplitSection.tsx:183). O onClick={() => removePerson(entry.uid)} contém um > dentro da arrow function, então [^>]*? fecha a tag cedo, o corpo do botão vira texto solto e o botão é classificado como "tem texto" — sai da lista. O gate nasceria com falso negativo justamente num dos sites que ele deve travar.
O parser precisa andar caractere a caractere pela tag de abertura, respeitando profundidade de {} e aspas (', ", `), para achar o > que realmente fecha a tag. Não é [^>].
Este é o único lugar do fatiamento onde esse parser é necessário. As asserções das fatias #126–#129 não precisam dele: elas recortam de <Button até <Icone, atravessando a arrow function sem interpretá-la. Concentrar o custo aqui foi deliberado.
Como validar que o gate não nasce inútil (exigência 3)
Um gate que passa no primeiro run não prova nada. Antes de abrir o PR, rodar contra dois estados:
- Com um dos 9 sites revertido — o scanner tem de acusar aquele site, nominalmente. Se passar, o detector não detecta.
- Especificamente com o site 9 revertido (
SplitSection.tsx, o botão removePerson) — é o caso que a regex ingênua deixa passar. É o teste do teste.
Sem os dois, a implementação mais provável (a regex [^>]*?) entra verde e o gate fica com um furo do tamanho exato do bug original.
Falsos positivos previsíveis, a tratar no PR
O scanner vai varrer botões legítimos com filho não-textual. Os que já existem hoje e precisam ficar fora da lista sem allowlist codificada:
- botões com ícone e texto (
<Plus /> Nova conta em AccountDialog, GroupDialog, GoalDialog, CategoryDialog) — a heurística "só ícones auto-fechados" já os exclui, mas confirmar;
- botões que já usam
sr-only (1 ocorrência no repo) ou title;
- o
DialogClose do DS (components/ui/dialog.tsx:43-46), que já rotula — serve de controle positivo: se o scanner o acusar, a heurística está errada.
Se sobrar algum caso genuíno que deva ser isento, o motivo vai em comentário no próprio teste, ao lado da exceção — não numa lista solta no topo.
Cobertura
Este item é a cobertura. A nota de cobertura das outras quatro fatias aponta para as asserções ancoradas por site; esta cobre a propriedade repo-wide.
Vale o registro de .claude/audit.md: "gate automático conta como teste" — não há @testing-library/react no projeto, e propor render de componente significaria propor a instalação da infra inteira, maior que a correção.
Impacto
Nenhum usuário é afetado hoje por esta issue isoladamente (as quatro anteriores é que corrigem o que o usuário sente). O impacto é sobre a próxima regressão: sem este gate, o 10º botão sem nome entra em main com CI verde, exatamente como os 9 primeiros.
Custo estimado
P — 1 arquivo: __tests__/unit/a11y-nomes-acessiveis.test.ts. O parser de tag de abertura é o grosso do trabalho (~60-100 linhas), não a varredura.
Fechamento
Este é o último item da #106. O PR que mergear este gate, com os 9 sites já corrigidos, pode usar closes #106 — a lista da seção Onde da #106 estará inteira (exigência 8). Antes disso, refs #106.
Fatia (d) de #106, e a única que não corrige um site — cria o gate que impede a recaída.
O problema que este gate resolve
As fatias #126–#129 corrigem os 9 sites conhecidos e cada uma traz uma asserção ancorada no seu próprio site (recorte
<Button ... <Icone, no padrão derow-actions.test.tsea11y-estado-selecao.test.ts). Essas asserções protegem contra regressão nos 9 sites, e só neles.Nada impede o 10º: um
<Button size="icon">novo, em arquivo novo, sem rótulo. O lint não pega —eslint.config.mjs:25-32escopajsx-a11y/label-has-associated-controlacomponents/ui/field.tsxe nada mais, e fora disso vale só o subconjunto doeslint-config-next/core-web-vitals, que não tem regra de nome acessível. O CI rodanpm run lint --max-warnings 0em todo push e ficou verde durante os meses em que os 9 sites existiram — essa é a evidência direta de que o gate atual não cobre a propriedade.O padrão se repetiu 9 vezes em 8 arquivos sem ninguém notar. É o critério para valer um gate estrutural em vez de mais uma asserção por arquivo.
Proposta
Estender
__tests__/unit/a11y-nomes-acessiveis.test.ts(criado pela #126) com uma varredura decomponents/**/*.tsxeapp/**/*.tsx: para cada<button>/<Button>, se o corpo contiver apenas componentes de ícone auto-fechados (sem texto, sem{expressão}que renderize texto), exigiraria-label,aria-labelledby,titleou um filhosr-only.A mensagem de falha deve listar
arquivo:linhade cada infrator — umtoEqual([])sobre array de strings já entrega isso no diff do Vitest, sem formatação extra.Armadilha de implementação que já custou uma rodada
A regex óbvia para a tag de abertura,
<(button|Button)\b([^>]*?)>, não encontra o site 9 da #106 (SplitSection.tsx:183). OonClick={() => removePerson(entry.uid)}contém um>dentro da arrow function, então[^>]*?fecha a tag cedo, o corpo do botão vira texto solto e o botão é classificado como "tem texto" — sai da lista. O gate nasceria com falso negativo justamente num dos sites que ele deve travar.O parser precisa andar caractere a caractere pela tag de abertura, respeitando profundidade de
{}e aspas (',",`), para achar o>que realmente fecha a tag. Não é[^>].Este é o único lugar do fatiamento onde esse parser é necessário. As asserções das fatias #126–#129 não precisam dele: elas recortam de
<Buttonaté<Icone, atravessando a arrow function sem interpretá-la. Concentrar o custo aqui foi deliberado.Como validar que o gate não nasce inútil (exigência 3)
Um gate que passa no primeiro run não prova nada. Antes de abrir o PR, rodar contra dois estados:
SplitSection.tsx, o botãoremovePerson) — é o caso que a regex ingênua deixa passar. É o teste do teste.Sem os dois, a implementação mais provável (a regex
[^>]*?) entra verde e o gate fica com um furo do tamanho exato do bug original.Falsos positivos previsíveis, a tratar no PR
O scanner vai varrer botões legítimos com filho não-textual. Os que já existem hoje e precisam ficar fora da lista sem allowlist codificada:
<Plus /> Nova contaemAccountDialog,GroupDialog,GoalDialog,CategoryDialog) — a heurística "só ícones auto-fechados" já os exclui, mas confirmar;sr-only(1 ocorrência no repo) outitle;DialogClosedo DS (components/ui/dialog.tsx:43-46), que já rotula — serve de controle positivo: se o scanner o acusar, a heurística está errada.Se sobrar algum caso genuíno que deva ser isento, o motivo vai em comentário no próprio teste, ao lado da exceção — não numa lista solta no topo.
Cobertura
Este item é a cobertura. A nota de cobertura das outras quatro fatias aponta para as asserções ancoradas por site; esta cobre a propriedade repo-wide.
Vale o registro de
.claude/audit.md: "gate automático conta como teste" — não há@testing-library/reactno projeto, e propor render de componente significaria propor a instalação da infra inteira, maior que a correção.Impacto
Nenhum usuário é afetado hoje por esta issue isoladamente (as quatro anteriores é que corrigem o que o usuário sente). O impacto é sobre a próxima regressão: sem este gate, o 10º botão sem nome entra em
maincom CI verde, exatamente como os 9 primeiros.Custo estimado
P — 1 arquivo:
__tests__/unit/a11y-nomes-acessiveis.test.ts. O parser de tag de abertura é o grosso do trabalho (~60-100 linhas), não a varredura.Fechamento
Este é o último item da #106. O PR que mergear este gate, com os 9 sites já corrigidos, pode usar
closes #106— a lista da seção Onde da #106 estará inteira (exigência 8). Antes disso,refs #106.