Discussion

Auditeur de nettoyage des indicateurs de fonctionnalités obsolètes

De Wikiprompt, l’encyclopédie libre de prompts

Fatih Kadir Akın

5 nov. 2026

Auditeur de nettoyage des indicateurs de fonctionnalités obsolètes Une compétence IA complète qui audite les feature flags par rapport à votre codebase, détecte les flags obsolètes, expirés ou avec des fautes de frappe, et produit un plan de nettoyage priorisé avec des références fichier:ligne. Inclut un prompt système complet, des documents de référence, des modèles, des exemples et un script Python autonome de scan.

Contenu du PromptEnregistrer

🌐
--- name: feature-flag-stale-cleanup-auditor description: Audite les feature flags par rapport à votre base de code et planifie leur nettoyage - détecte les flags de release à 100% activés ou désactivés pendant des semaines, les déploiements bloqués à un pourcentage partiel, les expériences qui ne se sont jamais terminées, les flags expirés, archivés et non référencés, les clés utilisées dans le code mais absentes du service de flags (ou mal orthographiées), les écarts entre la production et la préproduction et les flags sans propriétaire. Liste chaque fichier:ligne à modifier avec l'effort requis. Inclut un scanner Python stdlib hors ligne. --- # Auditeur de nettoyage des feature flags obsolètes Vous aidez les équipes d'ingénierie à réduire leur dette de feature flags. Les flags sont faciles à ajouter et personne ne les supprime : un flag de release reste à 100% pendant des mois avec l'ancien chemin de code toujours compilé, un déploiement s'arrête à 25% et la moitié des utilisateurs obtiennent un produit différent, un test A/B dure six mois, et une clé mal orthographiée dans le code renvoie silencieusement la valeur par défaut du SDK, de sorte qu'une fonctionnalité "lancée" n'est jamais affichée. Vous comparez l'inventaire des flags avec le code, décidez pour chaque flag s'il faut le supprimer (en conservant le chemin ON ou OFF), le finaliser, le décider ou l'archiver, et vous fournissez un plan de nettoyage avec les références exactes à modifier. Vous travaillez à partir d'un inventaire de flags exporté ou écrit à la main et des dossiers source qui vous sont donnés. Vous ne modifiez pas les flags dans le service de flags : l'équipe n'archive un flag qu'après que le changement de code est déployé partout. ## Fichiers dans cette compétence - `scripts/flag_audit.py` - lit un JSON de flags (clé, type release / experiment / ops / kill-switch / permission, propriétaire, créé, expire, archivé, par environnement activé / déploiement / mis à jour) et analyse les dossiers source (.ts .tsx .js .jsx .py .go .rb .java .kt .cs .php .swift .vue .svelte; ignore node_modules, dist, vendor) pour les appels SDK, les littéraux de chaîne et les propriétés `flags.camelCase`; signale FF01-FF14 et un plan de nettoyage classé avec les références fichier:ligne et l'effort (bibliothèque standard Python 3.8+ uniquement) - `references/flag-lifecycle-and-removal.md` - types de flags et leur durée de vie attendue, comment supprimer un flag en toute sécurité (conserver ON / conserver OFF), exporter les inventaires depuis les services de flags courants, et les politiques pour empêcher la dette de flags de revenir - `references/checks-and-codes.md` - chaque code de constatation, sa gravité et quoi faire, plus le format d'entrée - `templates/cleanup-ticket.md` - le ticket de nettoyage / la description de PR pour chaque flag - `examples/flags.json` - neuf flags d'une boutique en ligne (release, experiment, kill-switch, permission, archivé) - `examples/src/checkout.ts`, `examples/src/Header.tsx`, `examples/src/search.py` - code qui les utilise, y compris une clé mal orthographiée, une clé qui n'existe pas et une clé construite à l'exécution - `examples/cleanup-report.md` - la sortie réelle du scanner sur l'exemple ## Flux de travail ### 1. Collecter l'inventaire et le code Demandez quel service de flags ils utilisent et pour une exportation de tous les flags avec le type, le propriétaire, la date de création, l'expiration ou le ticket, et l'état par environnement (activé/désactivé, pourcentage de déploiement, dernier changement). Mappez-le dans un JSON comme `examples/flags.json` (voir la référence pour les noms de champs LaunchDarkly, Unleash, Flagsmith, GrowthBook et ConfigCat). Demandez les référentiels ou dossiers qui lisent les flags et le nom de leur fonction de vérification de flag si ce n'est pas un appel SDK courant. ### 2. Exécuter le 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 ``` Sortie 0 = aucune constatation haute, 1 = constatations hautes, 2 = mauvaise entrée. Sans shell, recherchez chaque clé dans le code (`git grep -n "<clé>"`) et appliquez les règles dans `references/checks-and-codes.md` à la main. ### 3. Lire le résultat - Haut en premier : FF01 clés dans le code que le service de flags ne connaît pas (les fautes de frappe lisent la valeur par défaut, donc la fonctionnalité est effectivement désactivée) et FF14 flags archivés toujours dans le code. - Moyen : FF02 entièrement activé et FF03 entièrement désactivé depuis plus longtemps que la fenêtre d'obsolescence, FF12 déploiements bloqués, FF09 expériences longues, FF05 expiré, FF08 production en avance sur la préproduction. - Bas/info : flags non référencés (vérifiez d'abord les autres référentiels), propriétaire ou expiration manquants, flags à longue durée de vie (kill-switches, permissions) listés mais non jugés obsolètes, clés construites à l'exécution que la recherche ne peut pas trouver. ### 4. Planifier le nettoyage Descendez le tableau du plan : corrigez d'abord les fautes de frappe, puis supprimez les flags avec peu de références (effort S) pour créer une dynamique. Pour "conserver ON", supprimez la vérification et la branche OFF et tout code qu'elle seule utilisait ; pour "conserver OFF", supprimez le code protégé. Les déploiements bloqués et les expériences nécessitent une décision du propriétaire avant les changements de code. Remplissez `templates/cleanup-ticket.md` par flag. ### 5. Livrer Retournez le verdict, le plan classé avec les propriétaires et les listes fichier:ligne, les tickets, et deux ou trois suggestions de politique (dates d'expiration sur chaque flag temporaire, un créneau de nettoyage mensuel, une règle de lint ou une vérification CI qui échoue sur les clés inconnues). ## Exemple de résultat Trois constatations hautes : `new-checkout-flow-v3` est lu dans checkout.ts mais n'existe pas, `recs_carousel` dans search.py est une faute de frappe de `recs-carousel` - donc le carrousel qui est "100% activé" depuis 170 jours n'a jamais été affiché - et le `legacy-pdf-invoices` archivé est toujours vérifié. `new-checkout-flow` est à 100% activé depuis 86 jours et a expiré en septembre (2 références, conserver ON), la bannière des fêtes 2025 est désactivée depuis 303 jours (conserver OFF), `search-v2` est bloqué à 25% depuis 63 jours, et l'expérience de tarification a duré 199 jours. `dark-mode` est activé en production mais désactivé en préproduction. Les flags kill-switch et permission sont conservés. ## Règles - Ne recommandez jamais de supprimer un kill-switch, un flag ops ou un flag de permission parce qu'il est ancien ; ils sont à longue durée de vie par conception. - Un flag sans référence dans les dossiers analysés peut être utilisé ailleurs (applications mobiles, autres services, configuration) ; dites-le avant de suggérer de l'archiver. - Supprimez d'abord le code, déployez, puis archivez le flag dans le service - jamais dans l'autre sens. - Un déploiement bloqué ou une expérience en cours nécessite la décision du propriétaire ; ne choisissez pas le gagnant à sa place. - Traitez `today` et la fenêtre d'obsolescence comme des entrées ; dites quelles valeurs vous avez utilisées. FILE:references/flag-lifecycle-and-removal.md # Cycle de vie des flags et suppression sûre ## Types de flags et durée de vie attendue | Type | Objectif | Durée de vie attendue | Terminé quand | |---|---|---|---| | release | cacher le travail inachevé, déployer progressivement | jours à quelques semaines après 100% | 100% partout et stable pendant la fenêtre d'obsolescence | | experiment | test A/B ou multivarié | la durée du test (2-6 semaines typiquement) | une décision est prise | | ops | interrupteur opérationnel (cache activé/désactivé, taille de lot) | long | rarement ; revoir chaque année | | kill-switch | désactiver rapidement une dépendance risquée | permanent | jamais par l'âge | | permission | droit d'accès, accès bêta, fonctionnalité de plan | permanent (ou déplacer vers le système de facturation/droits) | décision produit | ## Supprimer un flag en toute sécurité 1. Confirmez l'état avec le propriétaire : entièrement activé (conserver ON) ou entièrement désactivé (conserver OFF), dans chaque environnement et chaque service qui le lit. 2. Trouvez toutes les références (la liste du scanner plus `git grep -n "<clé>"` dans les autres référentiels, applications mobiles, configuration d'infrastructure, tableaux de bord d'analyse et tests). 3. Conserver ON : remplacez la vérification par la branche ON, supprimez la branche OFF, puis supprimez le code que seule la branche OFF utilisait (anciens composants, points de terminaison, styles, gardes de migration). Conserver OFF : supprimez le code protégé et tout ce que seul lui utilisait. 4. Supprimez les tests et fixtures spécifiques au flag ; conservez les tests du comportement qui reste. 5. Déployez dans chaque environnement. Applications mobiles : attendez que les anciennes versions d'application qui lisent le flag soient sous votre seuil de support. 6. Archivez ensuite le flag dans le service de flags (archivez, ne supprimez pas, afin que l'historique reste). 7. Fermez le ticket avec les liens de PR. ## Pièges courants - Fautes de frappe et clés renommées : le SDK retourne la valeur par défaut pour les clés inconnues sans erreur, donc le code qui lit `recs_carousel` ne voit jamais `recs-carousel`. Une vérification CI qui compare les clés littérales avec l'inventaire détecte cela. - Archiver d'abord : un flag archivé retourne la valeur par défaut partout où il est encore lu - souvent le chemin OFF, c'est-à-dire une annulation instantanée d'une fonctionnalité lancée. - Déploiements bloqués : 25% pendant deux mois signifie deux produits à supporter et tester ; terminez ou annulez. - Les expériences sans date de fin continuent de fausser les métriques et les événements d'analyse. - Les clés construites à l'exécution (`"export-format-" + region`) ne peuvent pas être trouvées par recherche ; conservez un fichier de constantes listant chaque clé possible. ## Exporter l'inventaire - LaunchDarkly : REST `GET /api/v2/flags/{projectKey}?summary=0` -> `key`, `creationDate` (ms), `temporary`, `archived`, `maintainerId`/`_maintainer`, `tags`, `environments.<env>.on`, `.lastModified`, pondérations de déploiement dans `fallthrough.rollout.variations` (poids/1000 = pourcentage). Mappez `temporary: false` à `permanent: true`. - Unleash : `GET /api/admin/projects/<p>/features` -> `name`, `type` (release, experiment, operational, kill-switch, permission), `createdAt`, `stale`, `environments[].enabled`, stratégie de déploiement progressif `parameters.rollout`. - Flagsmith : `/api/v1/projects/<id>/features/` + `featurestates` par environnement (`enabled`, `updated_at`), pourcentage via les remplacements de segment. - GrowthBook : `/api/v1/features` -> `id`, `owner`, `dateCreated`, `archived`, `environments.<env>.enabled`, règle de déploiement `coverage` (0-1 -> pourcentage). - ConfigCat : API de gestion paramètres + valeurs par environnement ; les options de pourcentage donnent le déploiement. - Configuration interne (YAML/JSON) : une entrée par flag ; ajoutez créé/propriétaire/expire à la main à partir de git blame. ## Politiques pour maintenir la dette de flags basse - Chaque flag temporaire obtient un propriétaire, un ticket et une date d'expiration lors de sa création. - Le nettoyage fait partie de la définition de terminé : le ticket de suppression est créé avec le flag. - Une revue mensuelle de 30 minutes des flags utilisant cet audit ; un plafond sur les flags de release actifs par équipe. - CI : échouer sur les clés non présentes dans l'inventaire ; avertir sur les flags expirés encore référencés. FILE:references/checks-and-codes.md # Vérifications et codes de constatation | Code | Gravité | Vérification | Que faire | |---|---|---|---| | FF01 | haute | un appel de vérification de flag (isEnabled, variation, useFlag, is_enabled ...) utilise une clé qui n'est pas dans l'inventaire ; une correspondance proche est suggérée (faute de frappe, `_` vs `-`) | corriger la clé ou créer le flag ; jusqu'à présent, le SDK retourne la valeur par défaut | | FF02 | moyenne | flag de release à 100% dans l'environnement principal pendant `stale_days` ou plus | supprimer le flag, conserver le chemin ON | | FF03 | moyenne | flag de release ou d'expérimentation désactivé / 0% dans l'environnement principal pendant `stale_days` ou plus | supprimer le flag et le code protégé, ou décider de lancer | | FF04 | basse | aucune référence dans les fichiers analysés | vérifier les autres référentiels et services ; archiver si inutilisé | | FF05 | moyenne | date `expires` dépassée (flags temporaires) | supprimer ou convenir d'une nouvelle date | | FF06 | info | flag ops, kill-switch, permission (ou `permanent: true`) - non jugé obsolète | revoir chaque année | | FF07 | basse | pas de propriétaire | en assigner un | | FF08 | moyenne | environnement principal en avance sur préproduction/qa/test/dev, ou aucun état pour l'environnement principal | aligner les environnements | | FF09 | moyenne | expérience plus ancienne que `experiment_max_days` | décider, livrer le gagnant, supprimer | | FF10 | info | clé de flag construite à l'exécution (variable, concaténation, chaîne de modèle) | utiliser des clés littérales ou un fichier de constantes | | FF11 | basse | flag temporaire sans `expires` | définir une expiration | | FF12 | moyenne | déploiement de release entre 1 et 99% inchangé pendant `stale_days` | terminer (100%) ou annuler, puis supprimer | | FF13 | moyenne | clé définie deux fois dans l'inventaire | fusionner | | FF14 | haute | flag archivé dans le service mais toujours référencé dans le code | supprimer les références | ## Verdict - **N constatation(s) haute(s)** lorsque FF01/FF14 existent ; sinon **N flag(s) prêt(s) à supprimer** ; sinon **aucun flag obsolète**. ## Actions du plan (dans cet ordre) supprimer le code (archivé) -> supprimer, conserver ON -> supprimer, conserver OFF -> terminer le déploiement -> décider l'expérience -> archiver (aucune référence). Dans une action, les flags avec plus de références d'abord. Effort : S 1-2 références, M 3-6, L 7+. ## Comment les références sont trouvées 1. Appels : `<nom>(` avec un premier argument littéral (ou un littéral après un argument principal tel qu'un client ou un utilisateur), pour les noms intégrés plus `call_names` / `--call`. 2. Tout littéral de chaîne égal à une clé connue (fichiers de constantes, cartes de configuration). 3. `flags.`, `features.`, `featureFlags.`, `toggles.` suivis de la clé ou de sa forme camelCase (`new-search` -> `flags.newSearch`). Les commentaires qui contiennent la clé entre guillemets sont également comptés - vérifiez les lignes listées. ## JSON des flags Niveau supérieur : `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` par défaut à 100 quand activé et 0 quand désactivé ; `updated` est le dernier changement de cet environnement (repli sur `created`). FILE:templates/cleanup-ticket.md # Supprimer le feature flag `<clé>` **Propriétaire :** <team-payments> **Type :** <release> **Ticket :** <PAY-412> **Action :** <supprimer, conserver ON> **État :** <100% activé en production depuis 2026-08-11 (86 jours), 100% en préproduction> **Expiré :** <2026-09-30> ## Pourquoi <Entièrement déployé et stable depuis 86 jours ; le chemin OFF (ancien checkout) est du code mort.> ## Changement - [ ] `<examples/src/checkout.ts:4>` - remplacer la vérification par la branche ON (`renderOnePageCheckout`) - [ ] `<examples/src/checkout.ts:14>` - toujours utiliser `oneClickButton()` - [ ] supprimer le code que seul le chemin OFF utilisait : <renderLegacyCheckout, classicButton, styles de l'ancien checkout> - [ ] supprimer les tests et fixtures spécifiques au flag - [ ] rechercher dans les autres référentiels / applications mobiles : `git grep -n "<clé>"` ## Déploiement 1. Fusionner et déployer en préproduction, puis en production. 2. Surveiller <conversion du checkout, taux d'erreur> pendant <2 jours>. 3. Archiver `<clé>` dans <service de flags> (archiver, ne pas supprimer). ## Risques / questions - <L'application mobile lit-elle ce flag ? Version minimale supportée ?> - <Des tableaux de bord ou alertes clés sur le 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": "Nouveau checkout en une page", "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": "Recherche basée sur 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": "Bannière promo Noël 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": "Test A/B : forfait annuel en premier", "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": "Thème sombre", "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": "Désactiver les paiements par carte pendant une panne 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": "Export CSV pour les clients bêta", "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": "Ancien rendu PDF", "environments": {"production": {"on": false, "rollout": 0, "updated": "2026-02-01"}}}, {"key": "recs-carousel", "type": "release", "owner": "team-growth", "created": "2026-03-03", "description": "Carrousel de recommandations sur les pages produit", "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("Les paiements par carte sont suspendus. Veuillez réessayer bientôt."); } const oneClick = await flags.isEnabled("new-checkout-flow", user); return oneClick ? oneClickButton() : classicButton(); } export async function invoiceLink(order: Order) { // TODO supprimer après la migration 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="Soldes de Noël : 20% de réduction sur tout" />} <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("l'export est en version bêta") # clés d'expérience par région, 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 # Rapport d'audit de nettoyage des feature flags Aujourd'hui 2026-11-05, environnement principal `production`, obsolète après 30 jours. 9 flag(s) dans l'inventaire, 3 fichier(s) source analysés dans examples/src. **Verdict : 3 constatation(s) haute(s) - flags utilisés dans le code que le service de flags ne connaît pas** Candidats au nettoyage : 6 (4 supprimables maintenant, 4 référence(s) à modifier). Constatations : 3 hautes, 9 moyennes, 2 basses, 3 info. ## Plan de nettoyage | # | Flag | Type | Propriétaire | Principal | Action | Pourquoi | Réfs (fichiers) | Effort | Où | |---|---|---|---|---|---|---|---|---|---| | 1 | `legacy-pdf-invoices` | release | team-billing | archivé | **supprimer le code** | archivé mais toujours dans le code | 1 (1) | S | `examples/src/checkout.ts:20` | | 2 | `new-checkout-flow` | release | team-payments | 100% | **supprimer, conserver ON** | 100% activé depuis 86 jours | 2 (1) | S | `examples/src/checkout.ts:4`, `examples/src/checkout.ts:14` | | 3 | `recs-carousel` | release | team-growth | 100% | **supprimer, conserver ON** | 100% activé depuis 170 jours | 0 (0) | - | - | | 4 | `holiday-banner-2025` | release | team-growth | 0% | **supprimer, conserver OFF** | désactivé depuis 303 jours | 1 (1) | S | `examples/src/Header.tsx:5` | | 5 | `search-v2` | release | team-search | 25% | **terminer le déploiement** | bloqué à 25% | 1 (1) | S | `examples/src/search.py:5` | | 6 | `pricing-page-test` | experiment | team-growth | 50% | **décider l'expérience** | en cours depuis 199 jours | 1 (1) | S | `examples/src/Header.tsx:15` | Effort : S = 1-2 références, M = 3-6, L = 7 ou plus. "conserver ON" = supprimer la vérification du flag et la branche OFF ; "conserver OFF" = supprimer le code protégé. Archivez le flag dans le service de flags uniquement après que le changement de code est déployé partout. ## Constatations | Sév | Code | Flag | Constatation | Correction | |---|---|---|---|---| | haute | FF01 | `new-checkout-flow-v3` | utilisé dans le code (examples/src/checkout.ts:27) mais non défini dans l'inventaire des flags - le SDK retourne la valeur par défaut, donc ce chemin de code lit la mauvaise clé | faute de frappe de new-checkout-flow ? utilisez la clé exacte | | haute | FF01 | `recs_carousel` | utilisé dans le code (examples/src/search.py:20) mais non défini dans l'inventaire des flags - le SDK retourne la valeur par défaut, donc ce chemin de code lit la mauvaise clé | faute de frappe de recs-carousel ? utilisez la clé exacte | | haute | FF14 | `legacy-pdf-invoices` | le flag est archivé dans le service de flags mais toujours référencé dans 1 endroit(s) - il retourne maintenant la valeur par défaut | supprimer les références | | moyenne | FF02 | `new-checkout-flow` | flag de release 100% activé en production depuis 86 jours | supprimer le flag et conserver le chemin ON ; supprimer la branche OFF et l'ancien code | | moyenne | FF02 | `recs-carousel` | flag de release 100% activé en production depuis 170 jours | supprimer le flag et conserver le chemin ON ; supprimer la branche OFF et l'ancien code | | moyenne | FF03 | `holiday-banner-2025` | flag de release désactivé en production depuis 303 jours - le code protégé est mort | supprimer le flag et le code protégé (ou décider de le lancer) | | moyenne | FF05 | `holiday-banner-2025` | expiré le 2026-01-10 (il y a 299 jours) | supprimez-le maintenant ou convenez d'une nouvelle date avec le propriétaire | | moyenne | FF05 | `new-checkout-flow` | expiré le 2026-09-30 (il y a 36 jours) | supprimez-le maintenant ou convenez d'une nouvelle date avec le propriétaire | | moyenne | FF05 | `pricing-page-test` | expiré le 2026-07-01 (il y a 127 jours) | supprimez-le maintenant ou convenez d'une nouvelle date avec le propriétaire | | moyenne | FF08 | `dark-mode` | la production est à 100% mais la préproduction à 0% - la production exécute un chemin que la préproduction ne teste pas | aligner la préproduction avec la production | | moyenne | FF09 | `pricing-page-test` | expérience en cours depuis 199 jours (limite 90) | annoncez le résultat, livrez le gagnant, supprimez le flag | | moyenne | FF12 | `search-v2` | déploiement bloqué à 25% en production depuis 63 jours - les utilisateurs obtiennent deux versions | terminez le déploiement (100%) ou revenez à 0, puis supprimez | | basse | FF04 | `recs-carousel` | aucune référence dans 3 fichier(s) analysés | vérifiez les autres référentiels/services ; si inutilisé, archivez-le dans le service de flags | | basse | FF07 | `beta-reports-export` | pas de propriétaire | assignez une équipe ou une personne propriétaire | | info | FF06 | `beta-reports-export` | flag de permission - à longue durée de vie par conception, non vérifié pour l'obsolescence | revoir chaque année ; conserver une description et un propriétaire | | info | FF06 | `payments-kill-switch` | flag kill-switch - à longue durée de vie par conception, non vérifié pour l'obsolescence | revoir chaque année ; conserver une description et un propriétaire | | info | FF10 | `-` | clé de flag construite à l'exécution à examples/src/search.py:14 - les références ne peuvent pas être trouvées par recherche | préférez les clés littérales (ou un fichier de constantes) afin que les flags puissent être nettoyés | ## Références par flag | Flag | Références | |---|---| | `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 recherche couvre les littéraux de chaîne, les appels SDK courants et les propriétés `flags.camelCase` dans les dossiers analysés uniquement ; vérifiez les autres services, applications mobiles et référentiels de configuration avant d'archiver un flag. FILE:scripts/flag_audit.py #!/usr/bin/env python3 """Auditeur de nettoyage des feature flags obsolètes (stdlib uniquement, Python 3.8+). Lit un inventaire de feature flags (JSON exporté ou écrit à la main depuis LaunchDarkly, Unleash, Flagsmith, GrowthBook, ConfigCat ou une configuration interne) et analyse une arborescence source pour les références de flags. Trouve les flags de release entièrement activés (ou désactivés) depuis des semaines et pouvant être supprimés, les déploiements partiels bloqués, les flags expirés et orphelins, les expériences qui ne se sont jamais terminées, les flags utilisés dans le code mais absents du service de flags (ils retombent silencieusement sur la valeur par défaut), la dérive d'environnement et les flags sans propriétaire - et écrit un plan de nettoyage priorisé avec les références fichier:ligne exactes à modifier. Utilisation : flag_audit.py FLAGS.json SRC_DIR [SRC_DIR ...] [--today AAAA-MM-JJ] [--stale-days N] [--call NOM ...] [--json] flag_audit.py --self-test Sortie : 0 = aucune constatation haute, 1 = au moins une constatation haute, 2 = mauvaise entrée. """ 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 doit être AAAA-MM-JJ, reçu %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:]) # ---------------------------------------------------------------- chargement def load_flags(cfg, today=None, stale_days=None): if not isinstance(cfg, dict) or not isinstance(cfg.get("flags"), list): raise InputError("le fichier de flags doit être un objet JSON avec une liste \"flags\"") bad = set(cfg) - TOP_KEYS if bad: raise InputError("clé(s) de niveau supérieur inconnue(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 sans clé" % i) badk = set(f) - FLAG_KEYS if badk: raise InputError("flag %s : clé(s) inconnue(s) %s" % (f["key"], ", ".join(sorted(badk)))) t = (f.get("type") or "release").lower() if t not in TYPES: raise InputError("flag %s : le type doit être l'un de %s, reçu %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 env %s doit être un objet {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 env %s : rollout doit être 0-100, reçu %r" % (f["key"], e, r)) envs[e] = {"on": bool(st.get("on")), "rollout": float(r), "updated": pdate(st["updated"], "flag %s env %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): """part effective des utilisateurs recevant le flag : 0-100""" return st["rollout"] if st["on"] else 0.0 # ---------------------------------------------------------------- analyse def scan(dirs, keys, calls): """Retourne refs {clé: [(chemin, ligne, texte)]}, inconnues {littéral: [(chemin,ligne)]}, dynamiques [(chemin,ligne,texte)], fichiers""" 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("répertoire source introuvable : %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("impossible de lire %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 défini deux fois dans l'inventaire (le dernier est utilisé)", "fusionner les deux entrées") 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, "utilisé dans le code (%s) mais non défini dans l'inventaire des flags - le SDK retourne la valeur par défaut, donc ce chemin de code est %s" % (locs, "probablement jamais activé" if not close else "en train de lire la mauvaise clé"), ("faute de frappe de %s ? utilisez la clé exacte" % close) if close else "créez le flag ou supprimez la vérification") for p, i, txt in dynamic[:20]: add("info", "FF10", "-", "clé de flag construite à l'exécution à %s:%d - les références ne peuvent pas être trouvées par recherche" % (p, i), "préférez les clés littérales (ou un fichier de constantes) afin que les flags puissent être nettoyés") 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, "pas de propriétaire", "assignez une équipe ou une personne propriétaire") if f["archived"]: if r: add("high", "FF14", k, "le flag est archivé dans le service de flags mais toujours référencé dans %d endroit(s) - il retourne maintenant la valeur par défaut" % len(r), "supprimer les références") plan.append({"key": k, "type": f["type"], "owner": f["owner"] or "?", "state": "archivé", "action": "supprimer le code", "why": "archivé mais toujours dans le code", "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, "aucun état pour l'environnement principal %r" % prim, "ajoutez l'état %s ou corrigez primary_env" % prim) if f["permanent"]: add("info", "FF06", k, "flag de %s - à longue durée de vie par conception, non vérifié pour l'obsolescence" % f["type"], "revoir chaque année ; conserver une description et un propriétaire") if not r: add("low", "FF04", k, "aucune référence dans %d fichier(s) analysés" % nfiles, "vérifiez les autres référentiels/services ; si inutilisé, archivez-le dans le service de flags") action, why = action or "archiver", "aucune référence" 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, "expiré le %s (il y a %d jours)" % (f["expires"], (today - f["expires"]).days), "supprimez-le maintenant ou convenez d'une nouvelle date avec le propriétaire") if f["type"] == "release" and e == 100 and days is not None and days >= stale: add("medium", "FF02", k, "flag de release 100%% activé dans %s depuis %d jours%s" % (prim, days, "" if all_on else " (pas 100% dans chaque environnement)"), "supprimer le flag et conserver le chemin ON ; supprimer la branche OFF et l'ancien code") action, why = "supprimer, conserver ON", "100%% activé depuis %d jours" % 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 désactivé dans %s depuis %d jours - le code protégé est mort" % (f["type"], prim, days), "supprimer le flag et le code protégé (ou décider de le lancer)") action, why = "supprimer, conserver OFF", "désactivé depuis %d jours" % days elif 0 < e < 100 and days is not None and days >= stale and f["type"] == "release": add("medium", "FF12", k, "déploiement bloqué à %g%% dans %s depuis %d jours - les utilisateurs obtiennent deux versions" % (e, prim, days), "terminez le déploiement (100%) ou revenez à 0, puis supprimez") action, why = "terminer le déploiement", "bloqué à %g%%" % e if f["type"] == "experiment" and age is not None and age > c["exp_max"]: add("medium", "FF09", k, "expérience en cours depuis %d jours (limite %d)" % (age, c["exp_max"]), "annoncez le résultat, livrez le gagnant, supprimez le flag") action, why = action or "décider l'expérience", "en cours depuis %d jours" % age if not r: add("low", "FF04", k, "aucune référence dans %d fichier(s) analysés" % nfiles, "vérifiez les autres référentiels/services ; si inutilisé, archivez-le dans le service de flags") action, why = action or "archiver", why or "aucune référence" 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 à %g%% mais %s à %g%% - la production exécute un chemin que la préproduction ne teste pas" % (prim, e, en, eff(s2)), "alignez %s avec %s" % (en, prim)) if f["expires"] is None and f["type"] in ("release", "experiment") and action is None: add("low", "FF11", k, "flag temporaire de %s sans date d'expiration" % f["type"], "définissez expires (ex. 30-60 jours après le déploiement complet)") 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 = {"supprimer le code": 0, "supprimer, conserver ON": 1, "supprimer, conserver OFF": 2, "terminer le déploiement": 3, "décider l'expérience": 4, "archiver": 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("supprimer")] out = ["# Rapport d'audit de nettoyage des feature flags", "", "Aujourd'hui %s, environnement principal `%s`, obsolète après %d jours. %d flag(s) dans l'inventaire, %d fichier(s) source analysés dans %s." % ( r["today"], r["primary"], r["stale_days"], r["flags"], r["files"], ", ".join(src)), ""] if c["high"]: v = "%d constatation(s) haute(s) - flags utilisés dans le code que le service de flags ne connaît pas" % c["high"] elif removable: v = "%d flag(s) prêt(s) à supprimer" % len(removable) else: v = "aucun flag obsolète" out += ["**Verdict : %s**" % v, "", "Candidats au nettoyage : %d (%d supprimables maintenant, %d référence(s) à modifier). Constatations : %d hautes, %d moyennes, %d basses, %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 nettoyage", "", "| # | Flag | Type | Propriétaire | Principal | Action | Pourquoi | Réfs (fichiers) | Effort | Où |", "|---|---|---|---|---|---|---|---|---|---|"] for i, p in enumerate(r["plan"], 1): locs = ", ".join("`%s`" % x for x in p["locs"][:4]) + (" +%d de plus" % (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 += ["", "Effort : S = 1-2 références, M = 3-6, L = 7 ou plus. \"conserver ON\" = supprimer la vérification du flag et la branche OFF ; \"conserver OFF\" = supprimer le code protégé. Archivez le flag dans le service de flags uniquement après que le changement de code est déployé partout.", ""] out += ["## Constatations", "", "| Sév | Code | Flag | Constatation | Correction |", "|---|---|---|---|---|"] for s, code, k, msg, fix in r["findings"]: out.append("| %s | %s | `%s` | %s | %s |" % (s, code, k, msg.replace("|", "\\|"), fix.replace("|", "\\|"))) out += ["", "## Références par flag", "", "| Flag | Références |", "|---|---|"] for k, n in sorted(r["refs"].items()): out.append("| `%s` | %d |" % (k, n)) out += ["", "La recherche couvre les littéraux de chaîne, les appels SDK courants et les propriétés `flags.camelCase` dans les dossiers analysés uniquement ; vérifiez les autres services, applications mobiles et référentiels de configuration avant d'archiver 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("impossible de lire %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) # ---------------------------------------------------------------- auto-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% activé depuis 65 jours -> FF02", ("medium", "FF02", "new-checkout") in codes(r)) t("100% activé depuis 6 jours -> pas obsolète", ("medium", "FF02", "dark-mode") not in codes(r)) t("action du plan conserver ON", [p["action"] for p in r["plan"] if p["key"] == "new-checkout"] == ["supprimer, conserver ON"]) t("réfs trouvées via appel", 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("désactivé depuis 65 jours -> FF03 conserver OFF", ("medium", "FF03", "old-banner") in codes(r) and r["plan"][0]["action"] == "supprimer, conserver OFF") r = mk([rel("half", r=50)], {"c.js": "isEnabled('half')"}) t("déploiement bloqué -> FF12", ("medium", "FF12", "half") in codes(r)) r = mk([rel("new-checkout")], {"a.ts": "isEnabled('new_checkout')\n"}) t("clé non définie dans le code -> FF01 haute", ("high", "FF01", "new_checkout") in codes(r)) t("FF01 suggère la clé proche", any("faute de frappe de new-checkout" in f[4] for f in r["findings"])) t("FF01 seul signifie 0 réfs pour la vraie clé -> FF04", ("low", "FF04", "new-checkout") in codes(r)) r = mk([rel("ghost")], {"a.ts": "const a = 1\n"}) t("aucune réfs -> FF04 + archiver", ("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("expérience de plus de 90 jours -> FF09", ("medium", "FF09", "exp1") in codes(r)) r = mk([rel("exp2", expires="2026-10-01", up="2026-10-30")], {"a.ts": "isEnabled('exp2')"}) t("expiré -> FF05", ("medium", "FF05", "exp2") in codes(r)) r = mk([dict(rel("ks"), type="kill-switch", expires=None)], {"a.ts": "isEnabled('ks')"}) t("kill-switch ignoré -> FF06 info uniquement", [x[1] for x in codes(r)] == ["FF06"]) r = mk([dict(rel("p", up="2026-10-30"), owner="")], {"a.ts": "isEnabled('p')"}) t("pas de propriétaire -> 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("prod en avance sur la préproduction -> FF08", ("medium", "FF08", "d") in codes(r)) r = mk([rel("x")], {"a.ts": "const k = 'x' + region\nisEnabled(flagName)\n"}) t("clé dynamique -> FF10 info", ("info", "FF10", "-") in codes(r)) r = mk([rel("x")], {"a.py": "is_enabled('export-' + region)\n"}) t("littéral + variable est dynamique, pas non défini", ("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("propriété flags.camelCase comptée", 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 et fichiers non-code ignorés", r["refs"]["new-search"] == 0) r = mk([rel("t", up="2026-10-30", expires=None)], {"a.ts": "isEnabled('t')"}) t("pas d'expiration -> FF11", ("low", "FF11", "t") in codes(r)) r = mk([rel("arch", archived=True)], {"a.ts": "isEnabled('arch')"}) t("archivé mais référencé -> FF14 haute", ("high", "FF14", "arch") in codes(r) and r["plan"][0]["action"] == "supprimer le code") r = mk([rel("dup", up="2026-10-30"), rel("dup", up="2026-10-30")], {"a.ts": "isEnabled('dup')"}) t("clé en double -> 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("nom d'appel personnalisé trouve les clés non définies", ("high", "FF01", "nope") in codes(r)) r = mk([rel("stale14", up="2026-10-20")], {"a.ts": "isEnabled('stale14')"}, stale_days=14) t("paramètre stale_days respecté", ("medium", "FF02", "stale14") in codes(r)) for bad, name in (({"flags": [{"key": "a", "type": "temp"}]}, "mauvais type"), ({"flags": [{"key": "a", "environments": {"production": {"on": True, "rollout": 150}}}]}, "rollout > 100"), ({"flags": [{"key": "a", "colour": 1}]}, "clé de flag inconnue"), ({"flag": []}, "liste flags manquante")): try: load_flags(bad); t("rejeté : " + name, False) except InputError: t("rejeté : " + name, True) r = mk([rel("new-checkout")], src) t("le rapport a un verdict et un plan", "**Verdict :" in report(r, ["x"]) and "## Plan de nettoyage" in report(r, ["x"])) for n, ok in tests: print("%s %s" % ("PASS" if ok else "ÉCHEC", n)) p = sum(ok for _, ok in tests) print("%d/%d réussis" % (p, len(tests))) return 0 if p == len(tests) else 1 def main(argv=None): ap = argparse.ArgumentParser(description="Trouve les feature flags obsolètes et planifie leur suppression.") 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="nom de fonction de vérification de flag supplémentaire (répétable)") 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("donnez FLAGS.json et au moins un répertoire source") try: r = run(a.flags, a.src, a.today, a.stale_days, a.call) except InputError as e: print("erreur : %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())

Connectez-vous pour voir le prompt complet

Continuer avec:

En vous connectant, vous acceptez nos Conditions et Confidentialité

Utilisation

Ce prompt est conçu pour être utilisé avec coding. Copiez le contenu ci-dessus et collez-le dans votre outil d’IA préféré.

Pour de meilleurs résultats, personnalisez les espaces réservés (indiqués par des crochets ou des majuscules) selon vos besoins.

Références

Catégories :coding| prompts.chat| feature-flags| code-cleanup

Discussion