Discusión

Auditor de Limpieza de Flags de Funciones Obsoletos

De Wikiprompt, la enciclopedia libre de prompts

Fatih Kadir Akın

5 nov 2026

Auditor de Limpieza de Flags de Funciones Obsoletos Una habilidad completa de IA que audita los feature flags contra tu base de código, encuentra flags obsoletos, expirados o con errores tipográficos, y produce un plan de limpieza priorizado con referencias de archivo:línea. Incluye un prompt de sistema completo, documentos de referencia, plantillas, ejemplos y un script de escáner Python autocontenido.

Contenido del PromptGuardar

🌐
--- name: feature-flag-stale-cleanup-auditor description: Audita los feature flags contra tu código y planifica su limpieza - encuentra flags de release al 100% activados o desactivados durante semanas, rollouts atascados en un porcentaje parcial, experimentos que nunca terminaron, flags expirados, archivados y sin referencias, claves usadas en el código pero ausentes del servicio de flags (o mal escritas), desviación entre staging y producción y flags sin propietario. Lista cada archivo:línea a cambiar con el esfuerzo. Incluye un escáner offline de Python con la stdlib. --- # Auditor de Limpieza de Feature Flags Obsoletos Ayudas a los equipos de ingeniería a pagar la deuda de feature flags. Los flags son baratos de añadir y nadie los elimina: un flag de release permanece al 100% durante meses con la ruta de código antigua aún compilada, un rollout se detiene al 25% y la mitad de los usuarios obtienen un producto diferente, una prueba A/B se ejecuta durante medio año, y una clave mal escrita en el código devuelve silenciosamente el valor por defecto del SDK, por lo que una función "lanzada" nunca se muestra. Comparas el inventario de flags con el código, decides para cada flag si eliminarlo (conservando la ruta ON u OFF), finalizarlo, decidirlo o archivarlo, y entregas un plan de limpieza con las referencias exactas a cambiar. Trabajas a partir de un inventario de flags exportado o escrito a mano y las carpetas de código fuente que te dan. No cambias flags en el servicio de flags: el equipo archiva un flag solo después de que el cambio de código se haya desplegado en todas partes. ## Archivos en esta habilidad - `scripts/flag_audit.py` - lee un JSON de flags (clave, tipo release / experiment / ops / kill-switch / permission, propietario, creado, expira, archivado, por entorno on / rollout / actualizado) y escanea carpetas de código fuente (.ts .tsx .js .jsx .py .go .rb .java .kt .cs .php .swift .vue .svelte; omite node_modules, dist, vendor) en busca de llamadas al SDK, literales de cadena y propiedades `flags.camelCase`; reporta FF01-FF14 y un plan de limpieza clasificado con referencias archivo:línea y esfuerzo (solo biblioteca estándar de Python 3.8+) - `references/flag-lifecycle-and-removal.md` - tipos de flags y su vida útil esperada, cómo eliminar un flag de forma segura (conservar ON / conservar OFF), exportar inventarios de servicios de flags comunes y políticas que evitan que la deuda de flags regrese - `references/checks-and-codes.md` - cada código de hallazgo, su severidad y qué hacer, además del formato de entrada - `templates/cleanup-ticket.md` - el ticket de limpieza / descripción del PR para cada flag - `examples/flags.json` - nueve flags de una tienda web (release, experiment, kill-switch, permission, archivado) - `examples/src/checkout.ts`, `examples/src/Header.tsx`, `examples/src/search.py` - código que los usa, incluyendo una clave mal escrita, una clave que no existe y una clave construida en tiempo de ejecución - `examples/cleanup-report.md` - la salida real del escáner en el ejemplo ## Flujo de trabajo ### 1. Recopila el inventario y el código Pregunta qué servicio de flags usan y pide una exportación de todos los flags con tipo, propietario, fecha de creación, expiración o ticket, y estado por entorno (on/off, porcentaje de rollout, último cambio). Conviértelo en un JSON como `examples/flags.json` (consulta la referencia para los nombres de campos de LaunchDarkly, Unleash, Flagsmith, GrowthBook y ConfigCat). Pide los repositorios o carpetas que leen flags y el nombre de su función de verificación de flags si no es una llamada común del SDK. ### 2. Ejecuta el escáner ``` 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 ``` Salida 0 = sin hallazgos altos, 1 = hallazgos altos, 2 = entrada incorrecta. Sin un shell, busca en el código cada clave (`git grep -n "<key>"`) y aplica las reglas en `references/checks-and-codes.md` manualmente. ### 3. Lee el resultado - Altos primero: FF01 claves en el código que el servicio de flags no conoce (los errores tipográficos leen el valor por defecto, por lo que la función está efectivamente desactivada) y FF14 flags archivados aún en el código. - Medios: FF02 completamente activados y FF03 completamente desactivados durante más tiempo que la ventana de obsolescencia, FF12 rollouts atascados, FF09 experimentos largos, FF05 expirados, FF08 producción por delante de staging. - Bajos/información: flags sin referencias (verifica otros repositorios primero), falta propietario o expiración, flags de larga duración (kill switches, permisos) listados pero no juzgados obsoletos, claves construidas en tiempo de ejecución que la búsqueda no puede encontrar. ### 4. Planifica la limpieza Trabaja en la tabla del plan: corrige errores tipográficos primero, luego elimina flags con pocas referencias (esfuerzo S) para generar impulso. Para "conservar ON" elimina la verificación y la rama OFF y cualquier código que solo ella usara; para "conservar OFF" elimina el código protegido. Los rollouts atascados y los experimentos necesitan una decisión del propietario antes de los cambios de código. Completa `templates/cleanup-ticket.md` por flag. ### 5. Entrega Devuelve el veredicto, el plan clasificado con propietarios y listas archivo:línea, los tickets y dos o tres sugerencias de política (fechas de expiración en cada flag temporal, un espacio mensual de limpieza, una regla de lint o verificación de CI que falle con claves desconocidas). ## Resultado de ejemplo Tres hallazgos altos: `new-checkout-flow-v3` se lee en checkout.ts pero no existe, `recs_carousel` en search.py es un error tipográfico de `recs-carousel` - por lo que el carrusel que está "100% activado" durante 170 días nunca se ha mostrado - y el archivado `legacy-pdf-invoices` aún se verifica. `new-checkout-flow` ha estado 100% activado durante 86 días y expiró en septiembre (2 referencias, conservar ON), el banner de vacaciones 2025 ha estado desactivado durante 303 días (conservar OFF), `search-v2` está atascado al 25% durante 63 días, y el experimento de precios ha durado 199 días. `dark-mode` está activado en producción pero desactivado en staging. Los flags de kill switch y permiso se conservan. ## Reglas - Nunca recomiendes eliminar un kill switch, flag de ops o permiso porque sea antiguo; son de larga duración por diseño. - Un flag sin referencias en las carpetas escaneadas puede usarse en otro lugar (aplicaciones móviles, otros servicios, configuración); dilo antes de sugerir archivarlo. - Elimina el código primero, despliega, luego archiva el flag en el servicio - nunca al revés. - Un rollout atascado o un experimento en curso necesita la decisión del propietario; no elijas al ganador por ellos. - Trata `today` y la ventana de obsolescencia como entradas; di qué valores usaste. FILE:references/flag-lifecycle-and-removal.md # Ciclo de vida de los flags y eliminación segura ## Tipos de flags y vida útil esperada | Tipo | Propósito | Vida esperada | Terminado cuando | |---|---|---|---| | release | ocultar trabajo sin terminar, desplegar gradualmente | días a unas pocas semanas después del 100% | 100% en todas partes y estable durante la ventana de obsolescencia | | experiment | prueba A/B o multivariante | la duración de la prueba (2-6 semanas típicamente) | se toma una decisión | | ops | interruptor operativo (caché on/off, tamaño de lote) | largo | raramente; revisar anualmente | | kill-switch | apagar una dependencia riesgosa rápidamente | permanente | nunca por antigüedad | | permission | derecho, acceso beta, función del plan | permanente (o mover al sistema de facturación/derechos) | decisión de producto | ## Eliminar un flag de forma segura 1. Confirma el estado con el propietario: completamente activado (conservar ON) o completamente desactivado (conservar OFF), en cada entorno y cada servicio que lo lea. 2. Encuentra todas las referencias (la lista del escáner más `git grep -n "<key>"` en otros repositorios, aplicaciones móviles, configuración de infraestructura, paneles de análisis y pruebas). 3. Conservar ON: reemplaza la verificación con la rama ON, elimina la rama OFF, luego elimina el código que solo usaba la rama OFF (componentes antiguos, endpoints, estilos, guardas de migración). Conservar OFF: elimina el código protegido y cualquier cosa que solo él usara. 4. Elimina pruebas y fixtures específicos del flag; conserva pruebas del comportamiento que permanece. 5. Despliega en cada entorno. Aplicaciones móviles: espera hasta que las versiones antiguas de la aplicación que leen el flag estén por debajo de tu umbral de soporte. 6. Luego archiva el flag en el servicio de flags (archiva, no elimines, para que el historial se conserve). 7. Cierra el ticket con los enlaces del PR. ## Errores comunes - Errores tipográficos y claves renombradas: el SDK devuelve el valor por defecto para claves desconocidas sin error, por lo que el código que lee `recs_carousel` nunca ve `recs-carousel`. Una verificación de CI que compare claves literales con el inventario detecta esto. - Archivar primero: un flag archivado devuelve el valor por defecto en todo lo que aún lo lee - a menudo la ruta OFF, es decir, una reversión instantánea de una función lanzada. - Rollouts atascados: 25% durante dos meses significa dos productos que soportar y probar; termina o revierte. - Experimentos sin fecha de fin siguen sesgando métricas y eventos de análisis. - Claves construidas en tiempo de ejecución (`"export-format-" + region`) no se pueden encontrar con la búsqueda; mantén un archivo de constantes que liste cada clave posible. ## Exportar el inventario - LaunchDarkly: REST `GET /api/v2/flags/{projectKey}?summary=0` -> `key`, `creationDate` (ms), `temporary`, `archived`, `maintainerId`/`_maintainer`, `tags`, `environments.<env>.on`, `.lastModified`, pesos de rollout en `fallthrough.rollout.variations` (weight/1000 = porcentaje). Mapea `temporary: false` a `permanent: true`. - Unleash: `GET /api/admin/projects/<p>/features` -> `name`, `type` (release, experiment, operational, kill-switch, permission), `createdAt`, `stale`, `environments[].enabled`, estrategia de rollout gradual `parameters.rollout`. - Flagsmith: `/api/v1/projects/<id>/features/` + `featurestates` por entorno (`enabled`, `updated_at`), porcentaje mediante anulaciones de segmento. - GrowthBook: `/api/v1/features` -> `id`, `owner`, `dateCreated`, `archived`, `environments.<env>.enabled`, regla de rollout `coverage` (0-1 -> porcentaje). - ConfigCat: API de gestión de configuraciones + valores por entorno; las opciones de porcentaje dan el rollout. - Config interna (YAML/JSON): una entrada por flag; añade creado/propietario/expira a mano desde git blame. ## Políticas que mantienen baja la deuda de flags - Cada flag temporal obtiene un propietario, un ticket y una fecha de expiración cuando se crea. - La limpieza es parte de la definición de terminado: el ticket de eliminación se crea con el flag. - Una revisión mensual de 30 minutos de flags usando esta auditoría; un límite de flags de release activos por equipo. - CI: falla con claves no presentes en el inventario; advierte sobre flags expirados aún referenciados. FILE:references/checks-and-codes.md # Verificaciones y códigos de hallazgo | Código | Severidad | Verificación | Qué hacer | |---|---|---|---| | FF01 | alta | una llamada de verificación de flag (isEnabled, variation, useFlag, is_enabled ...) usa una clave que no está en el inventario; se sugiere una coincidencia cercana (error tipográfico, `_` vs `-`) | corrige la clave o crea el flag; hasta entonces el SDK devuelve el valor por defecto | | FF02 | media | flag de release al 100% en el entorno principal durante `stale_days` o más | elimina el flag, conserva la ruta ON | | FF03 | media | flag de release o experimento desactivado / 0% en el entorno principal durante `stale_days` o más | elimina el flag y el código protegido, o decide lanzarlo | | FF04 | baja | sin referencia en los archivos escaneados | verifica otros repositorios y servicios; archiva si no se usa | | FF05 | media | fecha `expires` pasada (flags temporales) | elimina o acuerda una nueva fecha | | FF06 | info | flag de ops, kill-switch, permission (o `permanent: true`) - no juzgado obsoleto | revisar anualmente | | FF07 | baja | sin propietario | asigna uno | | FF08 | media | entorno principal por delante de staging/qa/test/dev, o sin estado para el entorno principal | alinea los entornos | | FF09 | media | experimento más antiguo que `experiment_max_days` | decide, lanza al ganador, elimina | | FF10 | info | clave de flag construida en tiempo de ejecución (variable, concatenación, plantilla de cadena) | usa claves literales o un archivo de constantes | | FF11 | baja | flag temporal sin `expires` | establece una expiración | | FF12 | media | rollout de release entre 1 y 99% sin cambios durante `stale_days` | termina (100%) o revierte, luego elimina | | FF13 | media | clave definida dos veces en el inventario | fusiona | | FF14 | alta | flag archivado en el servicio pero aún referenciado en el código | elimina las referencias | ## Veredicto - **N hallazgo(s) alto(s)** cuando existen FF01/FF14; si no, **N flag(s) listos para eliminar**; si no, **sin flags obsoletos**. ## Acciones del plan (en este orden) eliminar código (archivado) -> eliminar, conservar ON -> eliminar, conservar OFF -> terminar rollout -> decidir experimento -> archivar (sin referencias). Dentro de una acción, los flags con más referencias primero. Esfuerzo: S 1-2 referencias, M 3-6, L 7+. ## Cómo se encuentran las referencias 1. Llamadas: `<nombre>(` con un primer argumento literal (o un literal después de un argumento inicial como un cliente o usuario), para los nombres integrados más `call_names` / `--call`. 2. Cualquier literal de cadena igual a una clave conocida (archivos de constantes, mapas de configuración). 3. `flags.`, `features.`, `featureFlags.`, `toggles.` seguido de la clave o su forma camelCase (`new-search` -> `flags.newSearch`). Los comentarios que contienen la clave entre comillas también se cuentan - verifica las líneas listadas. ## JSON de flags Nivel 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` por defecto es 100 cuando on y 0 cuando off; `updated` es el último cambio de ese entorno (se usa `created` como respaldo). FILE:templates/cleanup-ticket.md # Eliminar el feature flag `<key>` **Propietario:** <team-payments> **Tipo:** <release> **Ticket:** <PAY-412> **Acción:** <eliminar, conservar ON> **Estado:** <100% activado en producción desde 2026-08-11 (86 días), 100% en staging> **Expirado:** <2026-09-30> ## Por qué <Completamente desplegado y estable durante 86 días; la ruta OFF (checkout heredado) es código muerto.> ## Cambio - [ ] `<examples/src/checkout.ts:4>` - reemplaza la verificación con la rama ON (`renderOnePageCheckout`) - [ ] `<examples/src/checkout.ts:14>` - usa siempre `oneClickButton()` - [ ] elimina el código que solo usaba la ruta OFF: <renderLegacyCheckout, classicButton, estilos del checkout heredado> - [ ] elimina pruebas y fixtures específicos del flag - [ ] busca en otros repositorios / aplicaciones móviles: `git grep -n "<key>"` ## Despliegue 1. Fusiona y despliega en staging, luego en producción. 2. Observa <conversión del checkout, tasa de error> durante <2 días>. 3. Archiva `<key>` en <servicio de flags> (archiva, no elimines). ## Riesgos / preguntas - <¿La aplicación móvil lee este flag? ¿Versión mínima soportada?> - <¿Algún panel o alerta vinculado al 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": "Nuevo checkout de una 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": "Búsqueda basada en 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 Navidad 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": "Prueba A/B: plan anual primero", "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 oscuro", "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": "Apagar pagos con tarjeta durante una caída del 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": "Exportación 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 PDF antiguo", "environments": {"production": {"on": false, "rollout": 0, "updated": "2026-02-01"}}}, {"key": "recs-carousel", "type": "release", "owner": "team-growth", "created": "2026-03-03", "description": "Carrusel de recomendaciones en páginas de producto", "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("Los pagos con tarjeta están en pausa. Inténtalo de nuevo en breve."); } const oneClick = await flags.isEnabled("new-checkout-flow", user); return oneClick ? oneClickButton() : classicButton(); } export async function invoiceLink(order: Order) { // TODO eliminar después de la migración 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="Venta navideña: 20% de descuento en todo" />} <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("la exportación está en beta") # claves de experimento por región, p. ej. "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 # Auditoría de limpieza de feature flags Hoy 2026-11-05, entorno principal `production`, obsoleto después de 30 días. 9 flag(s) en el inventario, 3 archivo(s) de código fuente escaneados en examples/src. **Veredicto: 3 hallazgo(s) alto(s) - flags usados en el código que el servicio de flags no conoce** Candidatos de limpieza: 6 (4 eliminables ahora, 4 referencia(s) a cambiar). Hallazgos: 3 altos, 9 medios, 2 bajos, 3 info. ## Plan de limpieza | # | Flag | Tipo | Propietario | Principal | Acción | Por qué | Refs (archivos) | Esfuerzo | Dónde | |---|---|---|---|---|---|---|---|---|---| | 1 | `legacy-pdf-invoices` | release | team-billing | archivado | **eliminar código** | archivado pero aún en el código | 1 (1) | S | `examples/src/checkout.ts:20` | | 2 | `new-checkout-flow` | release | team-payments | 100% | **eliminar, conservar ON** | 100% activado durante 86 días | 2 (1) | S | `examples/src/checkout.ts:4`, `examples/src/checkout.ts:14` | | 3 | `recs-carousel` | release | team-growth | 100% | **eliminar, conservar ON** | 100% activado durante 170 días | 0 (0) | - | - | | 4 | `holiday-banner-2025` | release | team-growth | 0% | **eliminar, conservar OFF** | desactivado durante 303 días | 1 (1) | S | `examples/src/Header.tsx:5` | | 5 | `search-v2` | release | team-search | 25% | **terminar rollout** | atascado al 25% | 1 (1) | S | `examples/src/search.py:5` | | 6 | `pricing-page-test` | experiment | team-growth | 50% | **decidir experimento** | ejecutándose 199 días | 1 (1) | S | `examples/src/Header.tsx:15` | Esfuerzo: S = 1-2 referencias, M = 3-6, L = 7 o más. "conservar ON" = eliminar la verificación del flag y la rama OFF; "conservar OFF" = eliminar el código protegido. Archiva el flag en el servicio de flags solo después de que el cambio de código esté desplegado en todas partes. ## Hallazgos | Sev | Código | Flag | Hallazgo | Corrección | |---|---|---|---|---| | alta | FF01 | `new-checkout-flow-v3` | usado en el código (examples/src/checkout.ts:27) pero no definido en el inventario de flags - el SDK devuelve el valor por defecto, por lo que esta ruta de código está leyendo la clave incorrecta | ¿error tipográfico de new-checkout-flow? usa la clave exacta | | alta | FF01 | `recs_carousel` | usado en el código (examples/src/search.py:20) pero no definido en el inventario de flags - el SDK devuelve el valor por defecto, por lo que esta ruta de código está leyendo la clave incorrecta | ¿error tipográfico de recs-carousel? usa la clave exacta | | alta | FF14 | `legacy-pdf-invoices` | el flag está archivado en el servicio de flags pero aún se referencia en 1 lugar(es) - ahora devuelve el valor por defecto | elimina las referencias | | media | FF02 | `new-checkout-flow` | flag de release 100% activado en producción durante 86 días | elimina el flag y conserva la ruta ON; elimina la rama OFF y el código antiguo | | media | FF02 | `recs-carousel` | flag de release 100% activado en producción durante 170 días | elimina el flag y conserva la ruta ON; elimina la rama OFF y el código antiguo | | media | FF03 | `holiday-banner-2025` | flag de release desactivado en producción durante 303 días - el código protegido está muerto | elimina el flag y el código protegido (o decide lanzarlo) | | media | FF05 | `holiday-banner-2025` | expirado el 2026-01-10 (hace 299 días) | elimínalo ahora o acuerda una nueva fecha con el propietario | | media | FF05 | `new-checkout-flow` | expirado el 2026-09-30 (hace 36 días) | elimínalo ahora o acuerda una nueva fecha con el propietario | | media | FF05 | `pricing-page-test` | expirado el 2026-07-01 (hace 127 días) | elimínalo ahora o acuerda una nueva fecha con el propietario | | media | FF08 | `dark-mode` | producción está al 100% pero staging al 0% - producción ejecuta una ruta que staging no prueba | alinea staging con producción | | media | FF09 | `pricing-page-test` | experimento ejecutándose durante 199 días (límite 90) | declara el resultado, lanza al ganador, elimina el flag | | media | FF12 | `search-v2` | rollout atascado al 25% en producción durante 63 días - los usuarios obtienen dos versiones | termina el rollout (100%) o revierte a 0, luego elimina | | baja | FF04 | `recs-carousel` | sin referencias en 3 archivo(s) escaneados | verifica otros repositorios/servicios; si no se usa, archívalo en el servicio de flags | | baja | FF07 | `beta-reports-export` | sin propietario | asigna un equipo o persona propietaria | | info | FF06 | `beta-reports-export` | flag de permiso - de larga duración por diseño, no se verifica obsolescencia | revisar anualmente; mantener una descripción y propietario | | info | FF06 | `payments-kill-switch` | flag de kill-switch - de larga duración por diseño, no se verifica obsolescencia | revisar anualmente; mantener una descripción y propietario | | info | FF10 | `-` | clave de flag construida en tiempo de ejecución en examples/src/search.py:14 - las referencias a ella no se pueden encontrar con la búsqueda | prefiere claves literales (o un archivo de constantes) para que los flags se puedan limpiar | ## Referencias por flag | Flag | Referencias | |---|---| | `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 | La búsqueda cubre literales de cadena, llamadas comunes del SDK y propiedades `flags.camelCase` solo en las carpetas escaneadas; verifica otros servicios, aplicaciones móviles y repositorios de configuración antes de archivar un flag. FILE:scripts/flag_audit.py #!/usr/bin/env python3 """Auditor de limpieza de feature flags obsoletos (solo stdlib, Python 3.8+). Lee un inventario de feature flags (JSON exportado o escrito a mano desde LaunchDarkly, Unleash, Flagsmith, GrowthBook, ConfigCat o una configuración interna) y escanea un árbol de código fuente en busca de referencias a flags. Encuentra flags de release que han estado completamente activados (o desactivados) durante semanas y se pueden eliminar, rollouts parciales que se atascaron, flags expirados y huérfanos, experimentos que nunca terminaron, flags usados en el código pero ausentes del servicio de flags (caen silenciosamente al valor por defecto), desviación de entornos y flags sin propietario - y escribe un plan de limpieza priorizado con las referencias exactas archivo:línea a cambiar. 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 Salida: 0 = sin hallazgos altos, 1 = al menos un hallazgo alto, 2 = entrada incorrecta. """ 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 debe ser YYYY-MM-DD, se obtuvo %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("el archivo de flags debe ser un objeto JSON con una lista \"flags\"") bad = set(cfg) - TOP_KEYS if bad: raise InputError("clave(s) de nivel superior desconocida(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("el flag #%d no tiene clave" % i) badk = set(f) - FLAG_KEYS if badk: raise InputError("flag %s: clave(s) desconocida(s) %s" % (f["key"], ", ".join(sorted(badk)))) t = (f.get("type") or "release").lower() if t not in TYPES: raise InputError("flag %s: el tipo debe ser uno de %s, se obtuvo %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 entorno %s debe ser un 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 entorno %s: rollout debe ser 0-100, se obtuvo %r" % (f["key"], e, r)) envs[e] = {"on": bool(st.get("on")), "rollout": float(r), "updated": pdate(st["updated"], "flag %s entorno %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): """proporción efectiva de usuarios que reciben el flag: 0-100""" return st["rollout"] if st["on"] else 0.0 # ---------------------------------------------------------------- scan def scan(dirs, keys, calls): """Devuelve refs {clave: [(ruta, línea, texto)]}, desconocidas {literal: [(ruta,línea)]}, dinámicas [(ruta,línea,texto)], archivos""" 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("directorio de código fuente no 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("no se puede leer %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 definido dos veces en el inventario (se usa el último)", "fusiona las dos 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, "usado en el código (%s) pero no definido en el inventario de flags - el SDK devuelve el valor por defecto, por lo que esta ruta de código está %s" % (locs, "probablemente nunca activada" if not close else "leyendo la clave incorrecta"), ("¿error tipográfico de %s? usa la clave exacta" % close) if close else "crea el flag o elimina la verificación") for p, i, txt in dynamic[:20]: add("info", "FF10", "-", "clave de flag construida en tiempo de ejecución en %s:%d - las referencias a ella no se pueden encontrar con la búsqueda" % (p, i), "prefiere claves literales (o un archivo de constantes) para que los flags se puedan limpiar") 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, "sin propietario", "asigna un equipo o persona propietaria") if f["archived"]: if r: add("high", "FF14", k, "el flag está archivado en el servicio de flags pero aún se referencia en %d lugar(es) - ahora devuelve el valor por defecto" % len(r), "elimina las referencias") plan.append({"key": k, "type": f["type"], "owner": f["owner"] or "?", "state": "archivado", "action": "eliminar código", "why": "archivado pero aún en el 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, "sin estado para el entorno principal %r" % prim, "añade el estado de %s o corrige primary_env" % prim) if f["permanent"]: add("info", "FF06", k, "flag de %s - de larga duración por diseño, no se verifica obsolescencia" % f["type"], "revisar anualmente; mantener una descripción y propietario") if not r: add("low", "FF04", k, "sin referencias en %d archivo(s) escaneados" % nfiles, "verifica otros repositorios/servicios; si no se usa, archívalo en el servicio de flags") action, why = action or "archivar", "sin referencias" 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, "expirado el %s (hace %d días)" % (f["expires"], (today - f["expires"]).days), "elimínalo ahora o acuerda una nueva fecha con el propietario") if f["type"] == "release" and e == 100 and days is not None and days >= stale: add("medium", "FF02", k, "flag de release 100%% activado en %s durante %d días%s" % (prim, days, "" if all_on else " (no 100% en cada entorno)"), "elimina el flag y conserva la ruta ON; elimina la rama OFF y el código antiguo") action, why = "eliminar, conservar ON", "100%% activado durante %d días" % 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 desactivado en %s durante %d días - el código protegido está muerto" % (f["type"], prim, days), "elimina el flag y el código protegido (o decide lanzarlo)") action, why = "eliminar, conservar OFF", "desactivado durante %d días" % days elif 0 < e < 100 and days is not None and days >= stale and f["type"] == "release": add("medium", "FF12", k, "rollout atascado al %g%% en %s durante %d días - los usuarios obtienen dos versiones" % (e, prim, days), "termina el rollout (100%) o revierte a 0, luego elimina") action, why = "terminar rollout", "atascado al %g%%" % e if f["type"] == "experiment" and age is not None and age > c["exp_max"]: add("medium", "FF09", k, "experimento ejecutándose durante %d días (límite %d)" % (age, c["exp_max"]), "declara el resultado, lanza al ganador, elimina el flag") action, why = action or "decidir experimento", "ejecutándose %d días" % age if not r: add("low", "FF04", k, "sin referencias en %d archivo(s) escaneados" % nfiles, "verifica otros repositorios/servicios; si no se usa, archívalo en el servicio de flags") action, why = action or "archivar", why or "sin referencias" 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á al %g%% pero %s al %g%% - producción ejecuta una ruta que staging no prueba" % (prim, e, en, eff(s2)), "alinea %s con %s" % (en, prim)) if f["expires"] is None and f["type"] in ("release", "experiment") and action is None: add("low", "FF11", k, "flag temporal de %s sin fecha de expiración" % f["type"], "establece expires (p. ej. 30-60 días después del despliegue 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 = {"eliminar código": 0, "eliminar, conservar ON": 1, "eliminar, conservar OFF": 2, "terminar rollout": 3, "decidir experimento": 4, "archivar": 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("eliminar")] out = ["# Auditoría de limpieza de feature flags", "", "Hoy %s, entorno principal `%s`, obsoleto después de %d días. %d flag(s) en el inventario, %d archivo(s) de código fuente escaneados en %s." % ( r["today"], r["primary"], r["stale_days"], r["flags"], r["files"], ", ".join(src)), ""] if c["high"]: v = "%d hallazgo(s) alto(s) - flags usados en el código que el servicio de flags no conoce" % c["high"] elif removable: v = "%d flag(s) listos para eliminar" % len(removable) else: v = "sin flags obsoletos" out += ["**Veredicto: %s**" % v, "", "Candidatos de limpieza: %d (%d eliminables ahora, %d referencia(s) a cambiar). Hallazgos: %d altos, %d medios, %d bajos, %d info." % ( len(r["plan"]), len(removable), sum(p["refs"] for p in removable), c["high"], c["medium"], c["low"], c["info"]), ""] out += ["## Plan de limpieza", "", "| # | Flag | Tipo | Propietario | Principal | Acción | Por qué | Refs (archivos) | Esfuerzo | Dónde |", "|---|---|---|---|---|---|---|---|---|---|"] for i, p in enumerate(r["plan"], 1): locs = ", ".join("`%s`" % x for x in p["locs"][:4]) + (" +%d más" % (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 += ["", "Esfuerzo: S = 1-2 referencias, M = 3-6, L = 7 o más. \"conservar ON\" = eliminar la verificación del flag y la rama OFF; \"conservar OFF\" = eliminar el código protegido. Archiva el flag en el servicio de flags solo después de que el cambio de código esté desplegado en todas partes.", ""] out += ["## Hallazgos", "", "| Sev | Código | Flag | Hallazgo | Corrección |", "|---|---|---|---|---|"] for s, code, k, msg, fix in r["findings"]: out.append("| %s | %s | `%s` | %s | %s |" % (s, code, k, msg.replace("|", "\\|"), fix.replace("|", "\\|"))) out += ["", "## Referencias por flag", "", "| Flag | Referencias |", "|---|---|"] for k, n in sorted(r["refs"].items()): out.append("| `%s` | %d |" % (k, n)) out += ["", "La búsqueda cubre literales de cadena, llamadas comunes del SDK y propiedades `flags.camelCase` solo en las carpetas escaneadas; verifica otros servicios, aplicaciones móviles y repositorios de configuración antes de archivar un 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("no se puede leer %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% activado durante 65 días -> FF02", ("medium", "FF02", "new-checkout") in codes(r)) t("100% activado durante 6 días -> no obsoleto", ("medium", "FF02", "dark-mode") not in codes(r)) t("acción del plan conservar ON", [p["action"] for p in r["plan"] if p["key"] == "new-checkout"] == ["eliminar, conservar ON"]) t("refs encontradas mediante llamada", 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("desactivado durante 65 días -> FF03 conservar OFF", ("medium", "FF03", "old-banner") in codes(r) and r["plan"][0]["action"] == "eliminar, conservar OFF") r = mk([rel("half", r=50)], {"c.js": "isEnabled('half')"}) t("rollout atascado -> FF12", ("medium", "FF12", "half") in codes(r)) r = mk([rel("new-checkout")], {"a.ts": "isEnabled('new_checkout')\n"}) t("clave no definida en el código -> FF01 alto", ("high", "FF01", "new_checkout") in codes(r)) t("FF01 sugiere la clave cercana", any("error tipográfico de new-checkout" in f[4] for f in r["findings"])) t("FF01 solo significa 0 refs para la clave real -> FF04", ("low", "FF04", "new-checkout") in codes(r)) r = mk([rel("ghost")], {"a.ts": "const a = 1\n"}) t("sin refs -> FF04 + archivar", ("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 más de 90 días -> FF09", ("medium", "FF09", "exp1") in codes(r)) r = mk([rel("exp2", expires="2026-10-01", up="2026-10-30")], {"a.ts": "isEnabled('exp2')"}) t("expirado -> FF05", ("medium", "FF05", "exp2") in codes(r)) r = mk([dict(rel("ks"), type="kill-switch", expires=None)], {"a.ts": "isEnabled('ks')"}) t("kill-switch omitido -> solo FF06 info", [x[1] for x in codes(r)] == ["FF06"]) r = mk([dict(rel("p", up="2026-10-30"), owner="")], {"a.ts": "isEnabled('p')"}) t("sin propietario -> 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("producción por delante de staging -> FF08", ("medium", "FF08", "d") in codes(r)) r = mk([rel("x")], {"a.ts": "const k = 'x' + region\nisEnabled(flagName)\n"}) t("clave dinámica -> FF10 info", ("info", "FF10", "-") in codes(r)) r = mk([rel("x")], {"a.py": "is_enabled('export-' + region)\n"}) t("literal + variable es dinámico, no no definido", ("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("propiedad 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 y archivos no de código omitidos", r["refs"]["new-search"] == 0) r = mk([rel("t", up="2026-10-30", expires=None)], {"a.ts": "isEnabled('t')"}) t("sin expiración -> FF11", ("low", "FF11", "t") in codes(r)) r = mk([rel("arch", archived=True)], {"a.ts": "isEnabled('arch')"}) t("archivado pero referenciado -> FF14 alto", ("high", "FF14", "arch") in codes(r) and r["plan"][0]["action"] == "eliminar código") r = mk([rel("dup", up="2026-10-30"), rel("dup", up="2026-10-30")], {"a.ts": "isEnabled('dup')"}) t("clave 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("nombre de llamada personalizado encuentra claves no definidas", ("high", "FF01", "nope") in codes(r)) r = mk([rel("stale14", up="2026-10-20")], {"a.ts": "isEnabled('stale14')"}, stale_days=14) t("configuración stale_days respetada", ("medium", "FF02", "stale14") in codes(r)) for bad, name in (({"flags": [{"key": "a", "type": "temp"}]}, "tipo incorrecto"), ({"flags": [{"key": "a", "environments": {"production": {"on": True, "rollout": 150}}}]}, "rollout > 100"), ({"flags": [{"key": "a", "colour": 1}]}, "clave de flag desconocida"), ({"flag": []}, "falta lista de flags")): try: load_flags(bad); t("rechazado: " + name, False) except InputError: t("rechazado: " + name, True) r = mk([rel("new-checkout")], src) t("el informe tiene veredicto y plan", "**Veredicto:" in report(r, ["x"]) and "## Plan de limpieza" 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 aprobados" % (p, len(tests))) return 0 if p == len(tests) else 1 def main(argv=None): ap = argparse.ArgumentParser(description="Encuentra feature flags obsoletos y planifica su eliminación.") 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="nombre adicional de función de verificación de flags (repetible)") 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("proporciona FLAGS.json y al menos un directorio de código fuente") try: r = run(a.flags, a.src, a.today, a.stale_days, a.call) except InputError as e: print("error: %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())

Iniciá sesión para ver el prompt completo

Continuar con:

Al iniciar sesión, aceptás nuestros Términos de uso y Política de privacidad

Uso

Este prompt está diseñado para usarse con coding. Copiá el contenido de arriba y pegalo en tu herramienta de IA preferida.

Para mejores resultados, personalizá los marcadores (indicados con corchetes o mayúsculas) con tus requisitos específicos.

Referencias

Categorías:coding| prompts.chat| feature-flags| code-cleanup

Discusión