---
name: feature-flag-stale-cleanup-auditor
description: Audita feature flags em relação ao seu código e planeja a limpeza - encontra flags de release 100% ligadas ou desligadas há semanas, rollouts travados em uma porcentagem parcial, experimentos que nunca terminaram, flags expiradas, arquivadas e sem referência, chaves usadas no código mas ausentes no serviço de flags (ou com erro de digitação), divergência entre staging e produção e flags sem dono. Lista cada arquivo:linha a ser alterado com o esforço. Inclui um scanner Python offline com stdlib.
---
# Auditor de Limpeza de Feature Flags Obsoletas
Você ajuda equipes de engenharia a pagar a dívida técnica de feature flags. Flags são baratas de adicionar e ninguém as remove: uma flag de release fica em 100% por meses com o caminho de código antigo ainda compilado, um rollout para em 25% e metade dos usuários recebe um produto diferente, um teste A/B roda por meio ano, e uma chave com erro de digitação no código silenciosamente retorna o padrão do SDK, então uma feature "lançada" nunca é exibida. Você compara o inventário de flags com o código, decide para cada flag se deve removê-la (mantendo o caminho ON ou OFF), finalizá-la, decidir sobre ela ou arquivá-la, e entrega um plano de limpeza com as referências exatas a serem alteradas.
Você trabalha a partir de um inventário de flags exportado ou escrito à mão e das pastas de código fornecidas. Você não altera flags no serviço de flags: a equipe arquiva uma flag somente após a alteração de código ser implantada em todos os lugares.
## Arquivos nesta skill
- `scripts/flag_audit.py` - lê um JSON de flags (chave, tipo release / experiment / ops / kill-switch / permission, dono, criado, expira, arquivado, por ambiente on / rollout / atualizado) e escaneia pastas de código (.ts .tsx .js .jsx .py .go .rb .java .kt .cs .php .swift .vue .svelte; ignora node_modules, dist, vendor) em busca de chamadas de SDK, literais de string e propriedades `flags.camelCase`; relata FF01-FF14 e um plano de limpeza ranqueado com referências arquivo:linha e esforço (apenas stdlib Python 3.8+)
- `references/flag-lifecycle-and-removal.md` - tipos de flag e sua vida útil esperada, como remover uma flag com segurança (manter ON / manter OFF), exportando inventários de serviços de flags comuns e políticas que impedem o retorno da dívida de flags
- `references/checks-and-codes.md` - cada código de achado, sua severidade e o que fazer, além do formato de entrada
- `templates/cleanup-ticket.md` - o ticket de limpeza / descrição de PR para cada flag
- `examples/flags.json` - nove flags de uma loja web (release, experiment, kill-switch, permission, arquivada)
- `examples/src/checkout.ts`, `examples/src/Header.tsx`, `examples/src/search.py` - código que as utiliza, incluindo uma chave com erro de digitação, uma chave que não existe e uma chave construída em tempo de execução
- `examples/cleanup-report.md` - a saída real do scanner no exemplo
## Fluxo de trabalho
### 1. Colete o inventário e o código
Pergunte qual serviço de flags eles usam e peça uma exportação de todas as flags com tipo, dono, data de criação, expiração ou ticket, e estado por ambiente (on/off, porcentagem de rollout, última alteração). Mapeie isso em um JSON como `examples/flags.json` (veja a referência para nomes de campos do LaunchDarkly, Unleash, Flagsmith, GrowthBook e ConfigCat). Peça os repositórios ou pastas que leem flags e o nome da sua função de verificação de flag se não for uma chamada comum de SDK.
### 2. Execute o scanner
```
python3 scripts/flag_audit.py examples/flags.json examples/src
python3 scripts/flag_audit.py flags.json web/src api/app --today 2026-11-05 --stale-days 21
python3 scripts/flag_audit.py flags.json src --call featureOn --call can
python3 scripts/flag_audit.py flags.json src --json
python3 scripts/flag_audit.py --self-test
```
Saída 0 = nenhum achado alto, 1 = achados altos, 2 = entrada inválida. Sem um shell, pesquise no código por cada chave (`git grep -n "<chave>"`) e aplique as regras em `references/checks-and-codes.md` manualmente.
### 3. Leia o resultado
- Alto primeiro: FF01 chaves no código que o serviço de flags não conhece (erros de digitação leem o padrão, então a feature está efetivamente desligada) e FF14 flags arquivadas ainda no código.
- Médio: FF02 totalmente ligada e FF03 totalmente desligada por mais tempo que a janela de obsolescência, FF12 rollouts travados, FF09 experimentos longos, FF05 expiradas, FF08 produção à frente do staging.
- Baixo/informação: flags sem referência (verifique outros repositórios primeiro), sem dono ou expiração, flags de longa duração (kill switches, permissões) listadas mas não julgadas obsoletas, chaves construídas em tempo de execução que a busca não encontra.
### 4. Planeje a limpeza
Trabalhe na tabela do plano: corrija erros de digitação primeiro, depois remova flags com poucas referências (esforço S) para ganhar impulso. Para "manter ON", exclua a verificação e o ramo OFF e qualquer código que só ele usava; para "manter OFF", exclua o código protegido. Rollouts travados e experimentos precisam de uma decisão do dono antes das alterações de código. Preencha `templates/cleanup-ticket.md` por flag.
### 5. Entregue
Retorne o veredito, o plano ranqueado com donos e listas arquivo:linha, os tickets e duas ou três sugestões de política (datas de expiração em toda flag temporária, um slot mensal de limpeza, uma regra de lint ou verificação de CI que falhe em chaves desconhecidas).
## Exemplo de resultado
Três achados altos: `new-checkout-flow-v3` é lido em checkout.ts mas não existe, `recs_carousel` em search.py é um erro de digitação de `recs-carousel` - então o carrossel que está "100% ligado" há 170 dias nunca foi exibido - e a arquivada `legacy-pdf-invoices` ainda é verificada. `new-checkout-flow` está 100% ligada há 86 dias e expirou em setembro (2 referências, manter ON), o banner de feriado de 2025 está desligado há 303 dias (manter OFF), `search-v2` está travada em 25% há 63 dias, e o experimento de preços roda há 199 dias. `dark-mode` está ligada em produção mas desligada em staging. Flags de kill switch e permissão são mantidas.
## Regras
- Nunca recomende remover uma flag de kill switch, ops ou permissão porque é antiga; elas são de longa duração por design.
- Uma flag sem referências nas pastas escaneadas pode ser usada em outro lugar (aplicativos móveis, outros serviços, configuração); diga isso antes de sugerir arquivá-la.
- Remova o código primeiro, implante, depois arquive a flag no serviço - nunca o contrário.
- Um rollout travado ou um experimento em andamento precisa da decisão do dono; não escolha o vencedor por eles.
- Trate `today` e a janela de obsolescência como entradas; diga quais valores você usou.
FILE:references/flag-lifecycle-and-removal.md
# Ciclo de vida de flags e remoção segura
## Tipos de flag e vida útil esperada
| Tipo | Propósito | Vida útil esperada | Concluído quando |
|---|---|---|---|
| release | ocultar trabalho inacabado, lançar gradualmente | dias a algumas semanas após 100% | 100% em todos os lugares e estável pela janela de obsolescência |
| experiment | teste A/B ou multivariado | a duração do teste (2-6 semanas típico) | uma decisão é tomada |
| ops | chave operacional (cache on/off, tamanho de lote) | longa | raramente; revisar anualmente |
| kill-switch | desligar uma dependência arriscada rapidamente | permanente | nunca por idade |
| permission | direito de acesso, acesso beta, recurso de plano | permanente (ou mover para o sistema de cobrança/direitos) | decisão de produto |
## Removendo uma flag com segurança
1. Confirme o estado com o dono: totalmente ligada (manter ON) ou totalmente desligada (manter OFF), em todos os ambientes e todos os serviços que a leem.
2. Encontre todas as referências (a lista do scanner mais `git grep -n "<chave>"` em outros repositórios, aplicativos móveis, configuração de infraestrutura, dashboards de análise e testes).
3. Manter ON: substitua a verificação pelo ramo ON, exclua o ramo OFF, depois exclua o código que apenas o ramo OFF usava (componentes antigos, endpoints, estilos, guardas de migração). Manter OFF: exclua o código protegido e qualquer coisa que só ele usava.
4. Exclua testes e fixtures específicos da flag; mantenha testes do comportamento que permanece.
5. Implante em todos os ambientes. Aplicativos móveis: aguarde até que versões antigas do aplicativo que leem a flag estejam abaixo do seu limite de suporte.
6. Então arquive a flag no serviço de flags (arquive, não exclua, para que o histórico permaneça).
7. Feche o ticket com os links dos PRs.
## Armadilhas comuns
- Erros de digitação e chaves renomeadas: o SDK retorna o padrão para chaves desconhecidas sem erro, então o código que lê `recs_carousel` nunca vê `recs-carousel`. Uma verificação de CI que compara chaves literais com o inventário detecta isso.
- Arquivar primeiro: uma flag arquivada retorna o padrão em todos os lugares que ainda a leem - geralmente o caminho OFF, ou seja, um rollback instantâneo de uma feature lançada.
- Rollouts travados: 25% por dois meses significa dois produtos para suportar e testar; finalize ou reverta.
- Experimentos sem data de término continuam distorcendo métricas e eventos de análise.
- Chaves construídas em tempo de execução (`"export-format-" + region`) não podem ser encontradas por busca; mantenha um arquivo de constantes listando todas as chaves possíveis.
## Exportando o inventário
- LaunchDarkly: REST `GET /api/v2/flags/{projectKey}?summary=0` -> `key`, `creationDate` (ms), `temporary`, `archived`, `maintainerId`/`_maintainer`, `tags`, `environments.<env>.on`, `.lastModified`, pesos de rollout em `fallthrough.rollout.variations` (weight/1000 = percentual). Mapeie `temporary: false` para `permanent: true`.
- Unleash: `GET /api/admin/projects/<p>/features` -> `name`, `type` (release, experiment, operational, kill-switch, permission), `createdAt`, `stale`, `environments[].enabled`, estratégia de rollout gradual `parameters.rollout`.
- Flagsmith: `/api/v1/projects/<id>/features/` + `featurestates` por ambiente (`enabled`, `updated_at`), percentual via overrides de segmento.
- GrowthBook: `/api/v1/features` -> `id`, `owner`, `dateCreated`, `archived`, `environments.<env>.enabled`, regra de rollout `coverage` (0-1 -> percentual).
- ConfigCat: API de gerenciamento de configurações + valores por ambiente; opções de percentual dão o rollout.
- Configuração interna (YAML/JSON): uma entrada por flag; adicione criado/dono/expira manualmente a partir do git blame.
## Políticas que mantêm a dívida de flags baixa
- Toda flag temporária ganha um dono, um ticket e uma data de expiração quando é criada.
- A limpeza faz parte da definição de pronto: o ticket de remoção é criado junto com a flag.
- Uma revisão mensal de 30 minutos de flags usando esta auditoria; um limite de flags de release ativas por equipe.
- CI: falhe em chaves não presentes no inventário; avise sobre flags expiradas ainda referenciadas.
FILE:references/checks-and-codes.md
# Verificações e códigos de achados
| Código | Severidade | Verificação | O que fazer |
|---|---|---|---|
| FF01 | alta | uma chamada de verificação de flag (isEnabled, variation, useFlag, is_enabled ...) usa uma chave que não está no inventário; uma correspondência próxima é sugerida (erro de digitação, `_` vs `-`) | corrija a chave ou crie a flag; até lá o SDK retorna o valor padrão |
| FF02 | média | flag de release em 100% no ambiente primário por `stale_days` ou mais | remova a flag, mantenha o caminho ON |
| FF03 | média | flag de release ou experiment desligada / 0% no ambiente primário por `stale_days` ou mais | remova a flag e o código protegido, ou decida lançar |
| FF04 | baixa | sem referência nos arquivos escaneados | verifique outros repositórios e serviços; arquive se não for usada |
| FF05 | média | data de `expires` passou (flags temporárias) | remova ou concorde uma nova data |
| FF06 | informação | flag de ops, kill-switch, permission (ou `permanent: true`) - não julgada obsoleta | revise anualmente |
| FF07 | baixa | sem dono | atribua um |
| FF08 | média | ambiente primário à frente de staging/qa/test/dev, ou sem estado para o ambiente primário | alinhe os ambientes |
| FF09 | média | experimento mais antigo que `experiment_max_days` | decida, lance o vencedor, remova |
| FF10 | informação | chave de flag construída em tempo de execução (variável, concatenação, template string) | use chaves literais ou um arquivo de constantes |
| FF11 | baixa | flag temporária sem `expires` | defina uma expiração |
| FF12 | média | rollout de release entre 1 e 99% inalterado por `stale_days` | finalize (100%) ou reverta, depois remova |
| FF13 | média | chave definida duas vezes no inventário | mescle |
| FF14 | alta | flag arquivada no serviço mas ainda referenciada no código | remova as referências |
## Veredito
- **N achado(s) alto(s)** quando FF01/FF14 existem; senão **N flag(s) pronta(s) para remover**; senão **nenhuma flag obsoleta**.
## Ações do plano (nesta ordem)
remover código (arquivada) -> remover, manter ON -> remover, manter OFF -> finalizar rollout -> decidir experimento -> arquivar (sem referências). Dentro de uma ação, flags com mais referências primeiro. Esforço: S 1-2 referências, M 3-6, L 7+.
## Como as referências são encontradas
1. Chamadas: `<nome>(` com um primeiro argumento literal (ou um literal após um argumento inicial, como um cliente ou usuário), para os nomes embutidos mais `call_names` / `--call`.
2. Qualquer literal de string igual a uma chave conhecida (arquivos de constantes, mapas de configuração).
3. `flags.`, `features.`, `featureFlags.`, `toggles.` seguido pela chave ou sua forma camelCase (`new-search` -> `flags.newSearch`).
Comentários que contêm a chave entre aspas também são contados - verifique as linhas listadas.
## JSON de flags
Nível superior: `today, primary_env (production), environments, stale_days (30), experiment_max_days (90), call_names[], flags[]`.
Flag: `key, type (release|experiment|ops|kill-switch|permission), owner, created, expires, permanent, archived, description, ticket, environments{name: {on, rollout 0-100, updated}}`. `rollout` assume 100 quando ligada e 0 quando desligada; `updated` é a última alteração daquele ambiente (recai para `created`).
FILE:templates/cleanup-ticket.md
# Remover feature flag `<chave>`
**Dono:** <team-payments> **Tipo:** <release> **Ticket:** <PAY-412> **Ação:** <remover, manter ON>
**Estado:** <100% ligada em produção desde 2026-08-11 (86 dias), 100% em staging> **Expirada:** <2026-09-30>
## Por quê
<Totalmente lançada e estável há 86 dias; o caminho OFF (checkout legado) é código morto.>
## Alteração
- [ ] `<examples/src/checkout.ts:4>` - substitua a verificação pelo ramo ON (`renderOnePageCheckout`)
- [ ] `<examples/src/checkout.ts:14>` - sempre use `oneClickButton()`
- [ ] exclua o código que apenas o caminho OFF usava: <renderLegacyCheckout, classicButton, estilos do checkout legado>
- [ ] exclua testes e fixtures específicos da flag
- [ ] pesquise outros repositórios / aplicativos móveis: `git grep -n "<chave>"`
## Lançamento
1. Mescle e implante em staging, depois produção.
2. Observe <conversão do checkout, taxa de erro> por <2 dias>.
3. Arquive `<chave>` em <serviço de flags> (arquive, não exclua).
## Riscos / perguntas
- <O aplicativo móvel lê esta flag? Versão mínima suportada?>
- <Algum dashboard ou alerta baseado na flag?>
FILE:examples/flags.json
{
"today": "2026-11-05",
"primary_env": "production",
"stale_days": 30,
"experiment_max_days": 90,
"flags": [
{"key": "new-checkout-flow", "type": "release", "owner": "team-payments", "created": "2026-06-02", "expires": "2026-09-30", "ticket": "PAY-412",
"description": "Novo checkout de uma página",
"environments": {"production": {"on": true, "rollout": 100, "updated": "2026-08-11"}, "staging": {"on": true, "rollout": 100, "updated": "2026-07-20"}}},
{"key": "search-v2", "type": "release", "owner": "team-search", "created": "2026-07-15", "expires": "2026-12-31",
"description": "Busca com suporte do Typesense",
"environments": {"production": {"on": true, "rollout": 25, "updated": "2026-09-03"}, "staging": {"on": true, "rollout": 100, "updated": "2026-08-01"}}},
{"key": "holiday-banner-2025", "type": "release", "owner": "team-growth", "created": "2025-11-10", "expires": "2026-01-10",
"description": "Banner promocional de Natal 2025",
"environments": {"production": {"on": false, "rollout": 0, "updated": "2026-01-06"}, "staging": {"on": false, "rollout": 0, "updated": "2026-01-06"}}},
{"key": "pricing-page-test", "type": "experiment", "owner": "team-growth", "created": "2026-04-20", "expires": "2026-07-01",
"description": "Teste A/B: plano anual primeiro",
"environments": {"production": {"on": true, "rollout": 50, "updated": "2026-04-22"}, "staging": {"on": true, "rollout": 50, "updated": "2026-04-22"}}},
{"key": "dark-mode", "type": "release", "owner": "team-web", "created": "2026-10-01", "expires": "2026-12-15",
"description": "Tema escuro",
"environments": {"production": {"on": true, "rollout": 100, "updated": "2026-10-28"}, "staging": {"on": false, "rollout": 0, "updated": "2026-10-02"}}},
{"key": "payments-kill-switch", "type": "kill-switch", "owner": "team-payments", "created": "2025-03-01",
"description": "Desligar pagamentos com cartão durante uma indisponibilidade do PSP",
"environments": {"production": {"on": true, "rollout": 100, "updated": "2025-03-01"}, "staging": {"on": true, "rollout": 100, "updated": "2025-03-01"}}},
{"key": "beta-reports-export", "type": "permission", "owner": "", "created": "2025-09-12",
"description": "Exportação CSV para clientes beta",
"environments": {"production": {"on": true, "rollout": 100, "updated": "2025-09-12"}}},
{"key": "legacy-pdf-invoices", "type": "release", "owner": "team-billing", "created": "2025-05-01", "archived": true,
"description": "Renderizador de PDF antigo",
"environments": {"production": {"on": false, "rollout": 0, "updated": "2026-02-01"}}},
{"key": "recs-carousel", "type": "release", "owner": "team-growth", "created": "2026-03-03",
"description": "Carrossel de recomendações nas páginas de produto",
"environments": {"production": {"on": true, "rollout": 100, "updated": "2026-05-19"}, "staging": {"on": true, "rollout": 100, "updated": "2026-05-01"}}}
]
}
FILE:examples/src/checkout.ts
import { flags } from "./flags";
export async function renderCheckout(user: User, cart: Cart) {
if (await flags.isEnabled("new-checkout-flow", user)) {
return renderOnePageCheckout(user, cart);
}
return renderLegacyCheckout(user, cart);
}
export async function payButton(user: User) {
if (await flags.isEnabled("payments-kill-switch", user)) {
return disabledButton("Pagamentos com cartão estão pausados. Tente novamente em instantes.");
}
const oneClick = await flags.isEnabled("new-checkout-flow", user);
return oneClick ? oneClickButton() : classicButton();
}
export async function invoiceLink(order: Order) {
// TODO remover após a migração de PDF
if (await flags.isEnabled("legacy-pdf-invoices")) {
return legacyPdfUrl(order.id);
}
return invoiceServiceUrl(order.id);
}
export async function trackCheckoutVariant(user: User) {
const variant = await flags.variation("new-checkout-flow-v3", user, "control");
analytics.track("checkout_variant", { variant });
}
FILE:examples/src/Header.tsx
import { useFlag } from "@acme/flags-react";
export function Header() {
const darkMode = useFlag("dark-mode");
const showBanner = useFlag("holiday-banner-2025");
return (
<header className={darkMode ? "header header--dark" : "header"}>
{showBanner && <PromoBanner text="Liquidação de feriado: 20% de desconto em tudo" />}
<SearchBox />
</header>
);
}
export function PricingPage({ user }: { user: User }) {
const annualFirst = useFlag("pricing-page-test");
return annualFirst ? <PlansAnnualFirst /> : <PlansMonthlyFirst />;
}
FILE:examples/src/search.py
from app.flags import flags
def search(query, user):
if flags.is_enabled("search-v2", user):
return typesense_search(query)
return postgres_search(query)
def export_reports(user, region):
if not flags.is_enabled("beta-reports-export", user):
raise PermissionError("a exportação está em beta")
# chaves de experimento por região, ex.: "export-format-eu"
if flags.is_enabled("export-format-" + region, user):
return export_parquet(user)
return export_csv(user)
def recommendations(product):
if flags.is_enabled("recs_carousel"):
return top_related(product, limit=12)
return []
FILE:examples/cleanup-report.md
# Auditoria de limpeza de feature flags
Hoje 2026-11-05, ambiente primário `production`, obsoleta após 30 dias. 9 flag(s) no inventário, 3 arquivo(s) de código escaneados em examples/src.
**Veredito: 3 achado(s) alto(s) - flags usadas no código que o serviço de flags não conhece**
Candidatas à limpeza: 6 (4 removíveis agora, 4 referência(s) a alterar). Achados: 3 altos, 9 médios, 2 baixos, 3 informações.
## Plano de limpeza
| # | Flag | Tipo | Dono | Primário | Ação | Por quê | Refs (arquivos) | Esforço | Onde |
|---|---|---|---|---|---|---|---|---|---|
| 1 | `legacy-pdf-invoices` | release | team-billing | arquivada | **remover código** | arquivada mas ainda no código | 1 (1) | S | `examples/src/checkout.ts:20` |
| 2 | `new-checkout-flow` | release | team-payments | 100% | **remover, manter ON** | 100% ligada há 86 dias | 2 (1) | S | `examples/src/checkout.ts:4`, `examples/src/checkout.ts:14` |
| 3 | `recs-carousel` | release | team-growth | 100% | **remover, manter ON** | 100% ligada há 170 dias | 0 (0) | - | - |
| 4 | `holiday-banner-2025` | release | team-growth | 0% | **remover, manter OFF** | desligada há 303 dias | 1 (1) | S | `examples/src/Header.tsx:5` |
| 5 | `search-v2` | release | team-search | 25% | **finalizar rollout** | travada em 25% | 1 (1) | S | `examples/src/search.py:5` |
| 6 | `pricing-page-test` | experiment | team-growth | 50% | **decidir experimento** | rodando há 199 dias | 1 (1) | S | `examples/src/Header.tsx:15` |
Esforço: S = 1-2 referências, M = 3-6, L = 7 ou mais. "manter ON" = excluir a verificação da flag e o ramo OFF; "manter OFF" = excluir o código protegido. Arquive a flag no serviço de flags somente após a alteração de código ser implantada em todos os lugares.
## Achados
| Sev | Código | Flag | Achado | Correção |
|---|---|---|---|---|
| alto | FF01 | `new-checkout-flow-v3` | usada no código (examples/src/checkout.ts:27) mas não definida no inventário de flags - o SDK retorna o padrão, então este caminho de código está lendo a chave errada | erro de digitação de new-checkout-flow? use a chave exata |
| alto | FF01 | `recs_carousel` | usada no código (examples/src/search.py:20) mas não definida no inventário de flags - o SDK retorna o padrão, então este caminho de código está lendo a chave errada | erro de digitação de recs-carousel? use a chave exata |
| alto | FF14 | `legacy-pdf-invoices` | flag arquivada no serviço de flags mas ainda referenciada em 1 lugar(es) - agora retorna o padrão | remova as referências |
| médio | FF02 | `new-checkout-flow` | flag de release 100% ligada em produção há 86 dias | remova a flag e mantenha o caminho ON; exclua o ramo OFF e o código antigo |
| médio | FF02 | `recs-carousel` | flag de release 100% ligada em produção há 170 dias | remova a flag e mantenha o caminho ON; exclua o ramo OFF e o código antigo |
| médio | FF03 | `holiday-banner-2025` | flag de release desligada em produção há 303 dias - o código protegido está morto | remova a flag e o código protegido (ou decida lançá-la) |
| médio | FF05 | `holiday-banner-2025` | expirada em 2026-01-10 (299 dias atrás) | remova agora ou concorde uma nova data com o dono |
| médio | FF05 | `new-checkout-flow` | expirada em 2026-09-30 (36 dias atrás) | remova agora ou concorde uma nova data com o dono |
| médio | FF05 | `pricing-page-test` | expirada em 2026-07-01 (127 dias atrás) | remova agora ou concorde uma nova data com o dono |
| médio | FF08 | `dark-mode` | produção está em 100% mas staging em 0% - produção executa um caminho que staging não testa | alinhe staging com produção |
| médio | FF09 | `pricing-page-test` | experimento rodando há 199 dias (limite 90) | declare o resultado, lance o vencedor, remova a flag |
| médio | FF12 | `search-v2` | rollout travado em 25% em produção há 63 dias - usuários recebem duas versões | finalize o rollout (100%) ou reverta para 0, depois remova |
| baixo | FF04 | `recs-carousel` | sem referências em 3 arquivo(s) escaneados | verifique outros repositórios/serviços; se não for usada, arquive-a no serviço de flags |
| baixo | FF07 | `beta-reports-export` | sem dono | atribua uma equipe ou pessoa dona |
| informação | FF06 | `beta-reports-export` | flag de permission - longa duração por design, não verificada para obsolescência | revise anualmente; mantenha uma descrição e dono |
| informação | FF06 | `payments-kill-switch` | flag de kill-switch - longa duração por design, não verificada para obsolescência | revise anualmente; mantenha uma descrição e dono |
| informação | FF10 | `-` | chave de flag construída em tempo de execução em examples/src/search.py:14 - referências a ela não podem ser encontradas por busca | prefira chaves literais (ou um arquivo de constantes) para que flags possam ser limpas |
## Referências por flag
| Flag | Referências |
|---|---|
| `beta-reports-export` | 1 |
| `dark-mode` | 1 |
| `holiday-banner-2025` | 1 |
| `legacy-pdf-invoices` | 1 |
| `new-checkout-flow` | 2 |
| `payments-kill-switch` | 1 |
| `pricing-page-test` | 1 |
| `recs-carousel` | 0 |
| `search-v2` | 1 |
A busca cobre literais de string, chamadas comuns de SDK e propriedades `flags.camelCase` apenas nas pastas escaneadas; verifique outros serviços, aplicativos móveis e repositórios de configuração antes de arquivar uma flag.
FILE:scripts/flag_audit.py
#!/usr/bin/env python3
"""Auditor de limpeza de feature flags obsoletas (apenas stdlib, Python 3.8+).
Lê um inventário de feature flags (JSON exportado ou escrito à mão a partir de LaunchDarkly, Unleash,
Flagsmith, GrowthBook, ConfigCat ou uma configuração interna) e escaneia uma árvore de código em busca de
referências a flags. Encontra flags de release totalmente ligadas (ou desligadas) há semanas que podem ser
removidas, rollouts parciais que travaram, flags expiradas e órfãs, experimentos que nunca terminaram,
flags usadas no código mas ausentes no serviço de flags (elas silenciosamente caem para o padrão),
divergência de ambientes e flags sem dono - e escreve um plano de limpeza priorizado com as
referências exatas arquivo:linha a alterar.
Uso:
flag_audit.py FLAGS.json SRC_DIR [SRC_DIR ...] [--today YYYY-MM-DD] [--stale-days N]
[--call NAME ...] [--json]
flag_audit.py --self-test
Saída: 0 = nenhum achado alto, 1 = pelo menos um achado alto, 2 = entrada inválida.
"""
import argparse, datetime, difflib, json, os, re, sys
SEV = {"high": 0, "medium": 1, "low": 2, "info": 3}
TYPES = {"release", "experiment", "ops", "kill-switch", "permission"}
LONG_LIVED = {"ops", "kill-switch", "permission"}
EXTS = {".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".rb", ".java", ".kt", ".cs", ".php", ".swift", ".vue", ".svelte"}
SKIP_DIRS = {"node_modules", ".git", "dist", "build", "vendor", "__pycache__", ".venv", "venv", "coverage"}
CALLS = ["isEnabled", "isFeatureEnabled", "variation", "boolVariation", "stringVariation", "useFlag", "useFeatureFlag",
"useFeatureIsOn", "getFlag", "getFeatureValue", "isOn", "feature_enabled", "is_enabled", "flag_enabled", "enabled?",
"Enabled", "IsEnabled", "getBooleanValue", "hasFeature"]
TOP_KEYS = {"today", "stale_days", "experiment_max_days", "primary_env", "environments", "flags", "call_names", "source"}
FLAG_KEYS = {"key", "type", "owner", "created", "expires", "permanent", "description", "environments", "archived", "ticket"}
class InputError(Exception):
pass
def pdate(s, what):
try:
return datetime.date.fromisoformat(str(s).strip()[:10])
except ValueError:
raise InputError("%s deve ser YYYY-MM-DD, recebido %r" % (what, s))
def camel(k):
parts = re.split(r"[-_.]", k)
return parts[0] + "".join(p[:1].upper() + p[1:] for p in parts[1:])
# ---------------------------------------------------------------- load
def load_flags(cfg, today=None, stale_days=None):
if not isinstance(cfg, dict) or not isinstance(cfg.get("flags"), list):
raise InputError("o arquivo de flags deve ser um objeto JSON com uma lista \"flags\"")
bad = set(cfg) - TOP_KEYS
if bad:
raise InputError("chave(s) de nível superior desconhecida(s): %s" % ", ".join(sorted(bad)))
c = {"today": pdate(today or cfg.get("today") or datetime.date.today().isoformat(), "today"),
"stale": int(stale_days if stale_days is not None else cfg.get("stale_days", 30)),
"exp_max": int(cfg.get("experiment_max_days", 90)),
"primary": cfg.get("primary_env", "production"),
"envs": cfg.get("environments") or [], "flags": {}, "dups": [], "calls": cfg.get("call_names") or []}
for i, f in enumerate(cfg["flags"], 1):
if not isinstance(f, dict) or not f.get("key"):
raise InputError("flag #%d não tem chave" % i)
badk = set(f) - FLAG_KEYS
if badk:
raise InputError("flag %s: chave(s) desconhecida(s) %s" % (f["key"], ", ".join(sorted(badk))))
t = (f.get("type") or "release").lower()
if t not in TYPES:
raise InputError("flag %s: tipo deve ser um de %s, recebido %r" % (f["key"], ", ".join(sorted(TYPES)), t))
envs = {}
for e, st in (f.get("environments") or {}).items():
if not isinstance(st, dict):
raise InputError("flag %s ambiente %s deve ser um objeto {on, rollout, updated}" % (f["key"], e))
r = st.get("rollout", 100 if st.get("on") else 0)
if not isinstance(r, (int, float)) or not 0 <= r <= 100:
raise InputError("flag %s ambiente %s: rollout deve ser 0-100, recebido %r" % (f["key"], e, r))
envs[e] = {"on": bool(st.get("on")), "rollout": float(r),
"updated": pdate(st["updated"], "flag %s ambiente %s updated" % (f["key"], e)) if st.get("updated") else None}
fl = {"key": f["key"], "type": t, "owner": f.get("owner") or "", "ticket": f.get("ticket", ""),
"created": pdate(f["created"], "flag %s created" % f["key"]) if f.get("created") else None,
"expires": pdate(f["expires"], "flag %s expires" % f["key"]) if f.get("expires") else None,
"permanent": bool(f.get("permanent")) or t in LONG_LIVED, "archived": bool(f.get("archived")),
"description": f.get("description", ""), "envs": envs}
if fl["key"] in c["flags"]:
c["dups"].append(fl["key"])
c["flags"][fl["key"]] = fl
return c
def eff(st):
"""parcela efetiva de usuários recebendo a flag: 0-100"""
return st["rollout"] if st["on"] else 0.0
# ---------------------------------------------------------------- scan
def scan(dirs, keys, calls):
"""Retorna refs {chave: [(caminho, linha, texto)]}, desconhecidas {literal: [(caminho,linha)]}, dinâmicas [(caminho,linha,texto)], arquivos"""
calls = sorted(set(CALLS + list(calls)), key=len, reverse=True)
alt = "|".join(re.escape(c) for c in calls)
call_re = re.compile(r"(?:\b|\.)(%s)\s*\(\s*(?:\w+\s*,\s*)?([\"'`])([^\"'`$]+)\2\s*[,)]" % alt)
dyn_re = re.compile(r"(?:\b|\.)(%s)\s*\(\s*(?![\"'`][^\"'`$]*[\"'`]\s*[,)])([^,)]+)[,)]" % alt)
lit_re = re.compile(r"([\"'`])([A-Za-z0-9][A-Za-z0-9_.:-]{2,80})\1")
camels = {camel(k): k for k in keys if camel(k) != k}
prop_re = re.compile(r"\b(?:flags|features|featureFlags|toggles)\.([A-Za-z_]\w*)")
refs, unknown, dynamic, nfiles = {k: [] for k in keys}, {}, [], 0
for d in dirs:
if not os.path.isdir(d):
raise InputError("diretório de código não encontrado: %s" % d)
for root, ds, fs in os.walk(d):
ds[:] = sorted(x for x in ds if x not in SKIP_DIRS)
for fn in sorted(fs):
if os.path.splitext(fn)[1] not in EXTS:
continue
p = os.path.join(root, fn)
nfiles += 1
try:
lines = open(p, encoding="utf-8", errors="replace").read().splitlines()
except OSError as e:
raise InputError("não foi possível ler %s: %s" % (p, e))
for i, ln in enumerate(lines, 1):
seen = set()
for m in call_re.finditer(ln):
lit = m.group(3)
if lit in refs:
refs[lit].append((p, i, ln.strip())); seen.add(lit)
else:
unknown.setdefault(lit, []).append((p, i, ln.strip()))
seen.add(lit)
for m in dyn_re.finditer(ln):
if m.group(2).strip() not in ("true", "false", "None", "null", ""):
dynamic.append((p, i, ln.strip()))
for m in lit_re.finditer(ln):
lit = m.group(2)
if lit in refs and lit not in seen:
refs[lit].append((p, i, ln.strip())); seen.add(lit)
for m in prop_re.finditer(ln):
k = camels.get(m.group(1)) or (m.group(1) if m.group(1) in refs else None)
if k and k not in seen:
refs[k].append((p, i, ln.strip())); seen.add(k)
return refs, unknown, dynamic, nfiles
# ---------------------------------------------------------------- audit
def audit(c, refs, unknown, dynamic, nfiles):
F, plan = [], []
today, stale, prim = c["today"], c["stale"], c["primary"]
add = lambda sev, code, key, msg, fix: F.append((sev, code, key, msg, fix))
for k in c["dups"]:
add("medium", "FF13", k, "flag definida duas vezes no inventário (a última é usada)", "mescle as duas entradas")
keys = list(c["flags"])
norm = {re.sub(r"[-_.]", "", k.lower()): k for k in keys}
for lit, where in sorted(unknown.items()):
close = norm.get(re.sub(r"[-_.]", "", lit.lower())) or (difflib.get_close_matches(lit, keys, 1, 0.8) or [None])[0]
locs = ", ".join("%s:%d" % (p, i) for p, i, _ in where[:3])
add("high", "FF01", lit, "usada no código (%s) mas não definida no inventário de flags - o SDK retorna o padrão, então este caminho de código está %s" %
(locs, "provavelmente nunca ligado" if not close else "lendo a chave errada"),
("erro de digitação de %s? use a chave exata" % close) if close else "crie a flag ou remova a verificação")
for p, i, txt in dynamic[:20]:
add("info", "FF10", "-", "chave de flag construída em tempo de execução em %s:%d - referências a ela não podem ser encontradas por busca" % (p, i), "prefira chaves literais (ou um arquivo de constantes) para que flags possam ser limpas")
for k, f in c["flags"].items():
r = refs.get(k, [])
files = sorted(set(p for p, _, _ in r))
st = f["envs"].get(prim)
age = (today - f["created"]).days if f["created"] else None
action, why = None, ""
if not f["owner"]:
add("low", "FF07", k, "sem dono", "atribua uma equipe ou pessoa dona")
if f["archived"]:
if r:
add("high", "FF14", k, "flag arquivada no serviço de flags mas ainda referenciada em %d lugar(es) - agora retorna o padrão" % len(r), "remova as referências")
plan.append({"key": k, "type": f["type"], "owner": f["owner"] or "?", "state": "arquivada", "action": "remover código",
"why": "arquivada mas ainda no código", "refs": len(r), "files": len(files),
"locs": ["%s:%d" % (p, i) for p, i, _ in r], "ticket": f["ticket"]})
continue
if st is None and not f["archived"]:
add("medium", "FF08", k, "sem estado para o ambiente primário %r" % prim, "adicione o estado de %s ou corrija primary_env" % prim)
if f["permanent"]:
add("info", "FF06", k, "flag de %s - longa duração por design, não verificada para obsolescência" % f["type"], "revise anualmente; mantenha uma descrição e dono")
if not r:
add("low", "FF04", k, "sem referências em %d arquivo(s) escaneados" % nfiles, "verifique outros repositórios/serviços; se não for usada, arquive-a no serviço de flags")
action, why = action or "arquivar", "sem referências"
elif st is not None:
since = st["updated"] or f["created"]
days = (today - since).days if since else None
e = eff(st)
all_on = all(eff(s) == 100 for s in f["envs"].values())
if f["expires"] and f["expires"] < today:
add("medium", "FF05", k, "expirada em %s (%d dias atrás)" % (f["expires"], (today - f["expires"]).days), "remova agora ou concorde uma nova data com o dono")
if f["type"] == "release" and e == 100 and days is not None and days >= stale:
add("medium", "FF02", k, "flag de release 100%% ligada em %s há %d dias%s" % (prim, days, "" if all_on else " (não 100% em todos os ambientes)"),
"remova a flag e mantenha o caminho ON; exclua o ramo OFF e o código antigo")
action, why = "remover, manter ON", "100%% ligada há %d dias" % days
elif e == 0 and days is not None and days >= stale and f["type"] in ("release", "experiment"):
add("medium", "FF03", k, "flag de %s desligada em %s há %d dias - o código protegido está morto" % (f["type"], prim, days),
"remova a flag e o código protegido (ou decida lançá-la)")
action, why = "remover, manter OFF", "desligada há %d dias" % days
elif 0 < e < 100 and days is not None and days >= stale and f["type"] == "release":
add("medium", "FF12", k, "rollout travado em %g%% em %s há %d dias - usuários recebem duas versões" % (e, prim, days), "finalize o rollout (100%) ou reverta para 0, depois remova")
action, why = "finalizar rollout", "travado em %g%%" % e
if f["type"] == "experiment" and age is not None and age > c["exp_max"]:
add("medium", "FF09", k, "experimento rodando há %d dias (limite %d)" % (age, c["exp_max"]), "declare o resultado, lance o vencedor, remova a flag")
action, why = action or "decidir experimento", "rodando %d dias" % age
if not r:
add("low", "FF04", k, "sem referências em %d arquivo(s) escaneados" % nfiles, "verifique outros repositórios/serviços; se não for usada, arquive-a no serviço de flags")
action, why = action or "arquivar", why or "sem referências"
for en, s2 in f["envs"].items():
if en != prim and eff(s2) < e and (en in ("staging", "qa", "test", "dev", "development")):
add("medium", "FF08", k, "%s está em %g%% mas %s em %g%% - produção executa um caminho que staging não testa" % (prim, e, en, eff(s2)), "alinhe %s com %s" % (en, prim))
if f["expires"] is None and f["type"] in ("release", "experiment") and action is None:
add("low", "FF11", k, "flag temporária de %s sem data de expiração" % f["type"], "defina expires (ex.: 30-60 dias após o rollout completo)")
if action:
plan.append({"key": k, "type": f["type"], "owner": f["owner"] or "?", "state": ("%g%%" % eff(st)) if st else "-", "action": action, "why": why,
"refs": len(r), "files": len(files), "locs": ["%s:%d" % (p, i) for p, i, _ in r], "ticket": f["ticket"]})
rank = {"remover código": 0, "remover, manter ON": 1, "remover, manter OFF": 2, "finalizar rollout": 3, "decidir experimento": 4, "arquivar": 5}
plan.sort(key=lambda x: (rank[x["action"]], -x["refs"], x["key"]))
F.sort(key=lambda x: (SEV[x[0]], x[1], x[2]))
return {"today": today.isoformat(), "primary": prim, "stale_days": stale, "flags": len(c["flags"]), "files": nfiles,
"refs": {k: len(v) for k, v in refs.items()}, "plan": plan, "findings": F}
def effort(n):
return "S" if n <= 2 else "M" if n <= 6 else "L"
def report(r, src):
c = {s: 0 for s in SEV}
for f in r["findings"]:
c[f[0]] += 1
removable = [p for p in r["plan"] if p["action"].startswith("remover")]
out = ["# Auditoria de limpeza de feature flags", "",
"Hoje %s, ambiente primário `%s`, obsoleta após %d dias. %d flag(s) no inventário, %d arquivo(s) de código escaneados em %s." % (
r["today"], r["primary"], r["stale_days"], r["flags"], r["files"], ", ".join(src)), ""]
if c["high"]:
v = "%d achado(s) alto(s) - flags usadas no código que o serviço de flags não conhece" % c["high"]
elif removable:
v = "%d flag(s) pronta(s) para remover" % len(removable)
else:
v = "nenhuma flag obsoleta"
out += ["**Veredito: %s**" % v, "", "Candidatas à limpeza: %d (%d removíveis agora, %d referência(s) a alterar). Achados: %d altos, %d médios, %d baixos, %d informações." % (
len(r["plan"]), len(removable), sum(p["refs"] for p in removable), c["high"], c["medium"], c["low"], c["info"]), ""]
out += ["## Plano de limpeza", "", "| # | Flag | Tipo | Dono | Primário | Ação | Por quê | Refs (arquivos) | Esforço | Onde |", "|---|---|---|---|---|---|---|---|---|---|"]
for i, p in enumerate(r["plan"], 1):
locs = ", ".join("`%s`" % x for x in p["locs"][:4]) + (" +%d mais" % (len(p["locs"]) - 4) if len(p["locs"]) > 4 else "")
out.append("| %d | `%s` | %s | %s | %s | **%s** | %s | %d (%d) | %s | %s |" % (i, p["key"], p["type"], p["owner"], p["state"], p["action"], p["why"],
p["refs"], p["files"], effort(p["refs"]) if p["refs"] else "-", locs or "-"))
out += ["", "Esforço: S = 1-2 referências, M = 3-6, L = 7 ou mais. \"manter ON\" = excluir a verificação da flag e o ramo OFF; \"manter OFF\" = excluir o código protegido. Arquive a flag no serviço de flags somente após a alteração de código ser implantada em todos os lugares.", ""]
out += ["## Achados", "", "| Sev | Código | Flag | Achado | Correção |", "|---|---|---|---|---|"]
for s, code, k, msg, fix in r["findings"]:
out.append("| %s | %s | `%s` | %s | %s |" % (s, code, k, msg.replace("|", "\\|"), fix.replace("|", "\\|")))
out += ["", "## Referências por flag", "", "| Flag | Referências |", "|---|---|"]
for k, n in sorted(r["refs"].items()):
out.append("| `%s` | %d |" % (k, n))
out += ["", "A busca cobre literais de string, chamadas comuns de SDK e propriedades `flags.camelCase` apenas nas pastas escaneadas; verifique outros serviços, aplicativos móveis e repositórios de configuração antes de arquivar uma flag."]
return "\n".join(out) + "\n"
def run(flags_path, dirs, today=None, stale=None, calls=()):
try:
cfg = json.load(open(flags_path, encoding="utf-8"))
except (OSError, ValueError) as e:
raise InputError("não foi possível ler %s: %s" % (flags_path, e))
c = load_flags(cfg, today, stale)
refs, unknown, dynamic, n = scan(dirs, list(c["flags"]), list(c["calls"]) + list(calls))
return audit(c, refs, unknown, dynamic, n)
# ---------------------------------------------------------------- self-test
def self_test():
import tempfile
tests = []
def t(name, cond):
tests.append((name, bool(cond)))
def mk(flags, files, **kw):
d = tempfile.mkdtemp()
for name, body in files.items():
os.makedirs(os.path.dirname(os.path.join(d, name)) or d, exist_ok=True)
open(os.path.join(d, name), "w").write(body)
cfg = {"today": "2026-11-05", "flags": flags}
cfg.update(kw)
c = load_flags(cfg)
refs, unk, dyn, n = scan([d], list(c["flags"]), c["calls"])
return audit(c, refs, unk, dyn, n)
def codes(r):
return [(f[0], f[1], f[2]) for f in r["findings"]]
P = lambda on, r, up: {"production": {"on": on, "rollout": r, "updated": up}}
rel = lambda k, on=True, r=100, up="2026-09-01", **x: dict({"key": k, "type": "release", "owner": "web", "created": "2026-08-01", "expires": "2027-01-01", "environments": P(on, r, up)}, **x)
src = {"a.ts": "if (client.isEnabled('new-checkout')) {}\nconst x = useFlag(\"dark-mode\")\n"}
r = mk([rel("new-checkout"), rel("dark-mode", up="2026-10-30")], src)
t("100% ligada há 65 dias -> FF02", ("medium", "FF02", "new-checkout") in codes(r))
t("100% ligada há 6 dias -> não obsoleta", ("medium", "FF02", "dark-mode") not in codes(r))
t("ação do plano manter ON", [p["action"] for p in r["plan"] if p["key"] == "new-checkout"] == ["remover, manter ON"])
t("refs encontradas via chamada", r["refs"]["new-checkout"] == 1 and r["refs"]["dark-mode"] == 1)
r = mk([rel("old-banner", on=False, r=0)], {"b.py": "if flags.is_enabled(\"old-banner\"):\n pass\n"})
t("desligada há 65 dias -> FF03 manter OFF", ("medium", "FF03", "old-banner") in codes(r) and r["plan"][0]["action"] == "remover, manter OFF")
r = mk([rel("half", r=50)], {"c.js": "isEnabled('half')"})
t("rollout travado -> FF12", ("medium", "FF12", "half") in codes(r))
r = mk([rel("new-checkout")], {"a.ts": "isEnabled('new_checkout')\n"})
t("chave indefinida no código -> FF01 alto", ("high", "FF01", "new_checkout") in codes(r))
t("FF01 sugere a chave próxima", any("erro de digitação de new-checkout" in f[4] for f in r["findings"]))
t("FF01 sozinho significa 0 refs para a chave real -> FF04", ("low", "FF04", "new-checkout") in codes(r))
r = mk([rel("ghost")], {"a.ts": "const a = 1\n"})
t("sem refs -> FF04 + arquivar", ("low", "FF04", "ghost") in codes(r))
r = mk([rel("exp1", type="experiment", created="2026-05-01", r=50, up="2026-10-20")], {"a.ts": "variation('exp1', false)"})
t("experimento acima de 90 dias -> FF09", ("medium", "FF09", "exp1") in codes(r))
r = mk([rel("exp2", expires="2026-10-01", up="2026-10-30")], {"a.ts": "isEnabled('exp2')"})
t("expirada -> FF05", ("medium", "FF05", "exp2") in codes(r))
r = mk([dict(rel("ks"), type="kill-switch", expires=None)], {"a.ts": "isEnabled('ks')"})
t("kill-switch ignorada -> apenas FF06 informação", [x[1] for x in codes(r)] == ["FF06"])
r = mk([dict(rel("p", up="2026-10-30"), owner="")], {"a.ts": "isEnabled('p')"})
t("sem dono -> FF07", ("low", "FF07", "p") in codes(r))
r = mk([dict(rel("d", up="2026-10-30"), environments={"production": {"on": True, "rollout": 100, "updated": "2026-10-30"}, "staging": {"on": False}})], {"a.ts": "isEnabled('d')"})
t("produção à frente do staging -> FF08", ("medium", "FF08", "d") in codes(r))
r = mk([rel("x")], {"a.ts": "const k = 'x' + region\nisEnabled(flagName)\n"})
t("chave dinâmica -> FF10 informação", ("info", "FF10", "-") in codes(r))
r = mk([rel("x")], {"a.py": "is_enabled('export-' + region)\n"})
t("literal + variável é dinâmico, não indefinido", ("info", "FF10", "-") in codes(r) and not [f for f in r["findings"] if f[1] == "FF01"])
r = mk([rel("new-search", up="2026-10-30")], {"s.tsx": "if (flags.newSearch) {}\n"})
t("propriedade flags.camelCase contada", r["refs"]["new-search"] == 1)
r = mk([rel("new-search", up="2026-10-30")], {"node_modules/x.js": "isEnabled('new-search')", "a.md": "isEnabled('new-search')"})
t("node_modules e arquivos não-código ignorados", r["refs"]["new-search"] == 0)
r = mk([rel("t", up="2026-10-30", expires=None)], {"a.ts": "isEnabled('t')"})
t("sem expiração -> FF11", ("low", "FF11", "t") in codes(r))
r = mk([rel("arch", archived=True)], {"a.ts": "isEnabled('arch')"})
t("arquivada mas referenciada -> FF14 alto", ("high", "FF14", "arch") in codes(r) and r["plan"][0]["action"] == "remover código")
r = mk([rel("dup", up="2026-10-30"), rel("dup", up="2026-10-30")], {"a.ts": "isEnabled('dup')"})
t("chave duplicada -> FF13", ("medium", "FF13", "dup") in codes(r))
r = mk([rel("cust", up="2026-10-30")], {"a.ts": "myFlags.check('cust')\nmyFlags.check('nope')\n"}, call_names=["check"])
t("nome de chamada personalizado encontra chaves indefinidas", ("high", "FF01", "nope") in codes(r))
r = mk([rel("stale14", up="2026-10-20")], {"a.ts": "isEnabled('stale14')"}, stale_days=14)
t("configuração stale_days respeitada", ("medium", "FF02", "stale14") in codes(r))
for bad, name in (({"flags": [{"key": "a", "type": "temp"}]}, "tipo inválido"), ({"flags": [{"key": "a", "environments": {"production": {"on": True, "rollout": 150}}}]}, "rollout > 100"),
({"flags": [{"key": "a", "colour": 1}]}, "chave de flag desconhecida"), ({"flag": []}, "lista de flags ausente")):
try:
load_flags(bad); t("rejeitado: " + name, False)
except InputError:
t("rejeitado: " + name, True)
r = mk([rel("new-checkout")], src)
t("relatório tem veredito e plano", "**Veredito:" in report(r, ["x"]) and "## Plano de limpeza" in report(r, ["x"]))
for n, ok in tests:
print("%s %s" % ("PASS" if ok else "FAIL", n))
p = sum(ok for _, ok in tests)
print("%d/%d passaram" % (p, len(tests)))
return 0 if p == len(tests) else 1
def main(argv=None):
ap = argparse.ArgumentParser(description="Encontre feature flags obsoletas e planeje sua remoção.")
ap.add_argument("flags", nargs="?")
ap.add_argument("src", nargs="*")
ap.add_argument("--today")
ap.add_argument("--stale-days", type=int)
ap.add_argument("--call", action="append", default=[], help="nome extra de função de verificação de flag (repetível)")
ap.add_argument("--json", action="store_true")
ap.add_argument("--self-test", action="store_true")
a = ap.parse_args(argv)
if a.self_test:
return self_test()
if not a.flags or not a.src:
ap.error("forneça FLAGS.json e pelo menos um diretório de código")
try:
r = run(a.flags, a.src, a.today, a.stale_days, a.call)
except InputError as e:
print("erro: %s" % e, file=sys.stderr)
return 2
if a.json:
print(json.dumps({**r, "findings": [dict(zip(("severity", "code", "flag", "finding", "fix"), f)) for f in r["findings"]]}, indent=2))
else:
sys.stdout.write(report(r, a.src))
return 1 if any(f[0] == "high" for f in r["findings"]) else 0
if __name__ == "__main__":
sys.exit(main())
Discussão
0 comentários