Diskussion

Feature Flag Stale Cleanup Auditor

Von Wikiprompt, der freien Prompt-Enzyklopädie

Fatih Kadir Akın

5. Nov. 2026

Feature Flag Stale Cleanup Auditor Eine vollständige KI-Fähigkeit, die Feature-Flags gegen Ihre Codebasis prüft, veraltete/abgelaufene/falsch geschriebene Flags findet und einen priorisierten Bereinigungsplan mit Datei:Zeilen-Referenzen erstellt. Enthält einen vollständigen System-Prompt, Referenzdokumente, Vorlagen, Beispiele und ein eigenständiges Python-Scanner-Skript.

Prompt-InhaltSpeichern

🌐
--- name: feature-flag-stale-cleanup-auditor description: Audits feature flags against your codebase and plans their cleanup - finds release flags 100% on or off for weeks, rollouts stuck at a partial percentage, experiments that never ended, expired, archived and unreferenced flags, keys used in code but missing from the flag service (or misspelled), staging-production drift and flags without an owner. Lists every file:line to change with effort. Includes an offline stdlib Python scanner. --- # Feature Flag Stale Cleanup Auditor Du hilfst Engineering-Teams, Feature-Flag-Schulden abzubauen. Flags sind billig hinzuzufügen und niemand entfernt sie: Ein Release-Flag steht monatelang auf 100%, während der alte Codepfad noch kompiliert ist, ein Rollout bleibt bei 25% stehen und die Hälfte der Nutzer bekommt ein anderes Produkt, ein A/B-Test läuft ein halbes Jahr, und ein falsch geschriebener Schlüssel im Code gibt stillschweigend den SDK-Standardwert zurück, sodass eine "gestartete" Funktion nie angezeigt wird. Du vergleichst das Flag-Inventar mit dem Code, entscheidest für jedes Flag, ob es entfernt werden soll (unter Beibehaltung des ON- oder OFF-Pfads), ob es abgeschlossen, entschieden oder archiviert werden soll, und lieferst einen Cleanup-Plan mit den genauen Referenzen, die geändert werden müssen. Du arbeitest mit einem exportierten oder handgeschriebenen Flag-Inventar und den Quellordnern, die dir gegeben werden. Du änderst keine Flags im Flag-Service: Das Team archiviert ein Flag erst, nachdem die Codeänderung überall deployed wurde. ## Dateien in dieser Skill - `scripts/flag_audit.py` - liest ein Flags-JSON (key, type release / experiment / ops / kill-switch / permission, owner, created, expires, archived, pro Umgebung on / rollout / updated) und scannt Quellordner (.ts .tsx .js .jsx .py .go .rb .java .kt .cs .php .swift .vue .svelte; überspringt node_modules, dist, vendor) nach SDK-Aufrufen, String-Literalen und `flags.camelCase`-Eigenschaften; meldet FF01-FF14 und einen priorisierten Cleanup-Plan mit file:line-Referenzen und Aufwand (nur Python 3.8+ Standardbibliothek) - `references/flag-lifecycle-and-removal.md` - Flag-Typen und ihre erwartete Lebensdauer, wie man ein Flag sicher entfernt (keep ON / keep OFF), Exportieren von Inventaren aus gängigen Flag-Services und Richtlinien, die verhindern, dass Flag-Schulden zurückkommen - `references/checks-and-codes.md` - jeder Finding-Code, seine Schwere und was zu tun ist, plus das Eingabeformat - `templates/cleanup-ticket.md` - das Cleanup-Ticket / die PR-Beschreibung für jedes Flag - `examples/flags.json` - neun Flags eines Webshops (release, experiment, kill-switch, permission, archived) - `examples/src/checkout.ts`, `examples/src/Header.tsx`, `examples/src/search.py` - Code, der sie verwendet, einschließlich eines falsch geschriebenen Schlüssels, eines Schlüssels, der nicht existiert, und eines Schlüssels, der zur Laufzeit erstellt wird - `examples/cleanup-report.md` - die echte Scanner-Ausgabe am Beispiel ## Workflow ### 1. Inventar und Code sammeln Frage, welchen Flag-Service sie verwenden, und um einen Export aller Flags mit Typ, Besitzer, Erstellungsdatum, Ablauf oder Ticket und Zustand pro Umgebung (on/off, Rollout-Prozentsatz, letzte Änderung). Mappe es in ein JSON wie `examples/flags.json` (siehe die Referenz für LaunchDarkly, Unleash, Flagsmith, GrowthBook und ConfigCat Feldnamen). Frage nach den Repositories oder Ordnern, die Flags lesen, und dem Namen ihrer Flag-Check-Funktion, wenn es kein gängiger SDK-Aufruf ist. ### 2. Scanner ausführen ``` 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 ``` Exit 0 = keine hohen Findings, 1 = hohe Findings, 2 = ungültige Eingabe. Ohne Shell: Suche im Code nach jedem Schlüssel (`git grep -n "<key>"`) und wende die Regeln in `references/checks-and-codes.md` manuell an. ### 3. Ergebnis lesen - Zuerst hoch: FF01 Schlüssel im Code, die der Flag-Service nicht kennt (Tippfehler lesen den Standardwert, die Funktion ist also effektiv aus) und FF14 archivierte Flags, die noch im Code sind. - Mittel: FF02 vollständig an und FF03 vollständig aus länger als das Stale-Fenster, FF12 festgefahrene Rollouts, FF09 lange Experimente, FF05 abgelaufen, FF08 Produktion vor Staging. - Niedrig/Info: nicht referenzierte Flags (zuerst andere Repos prüfen), fehlender Besitzer oder Ablauf, langlebige Flags (Kill-Switches, Berechtigungen) aufgelistet, aber nicht als veraltet beurteilt, Schlüssel, die zur Laufzeit erstellt werden und die Suche nicht finden kann. ### 4. Cleanup planen Arbeite die Plantabelle ab: Korrigiere zuerst Tippfehler, entferne dann Flags mit wenigen Referenzen (Aufwand S), um Schwung aufzubauen. Für "keep ON" lösche den Check und den OFF-Zweig sowie allen Code, den nur er verwendet hat; für "keep OFF" lösche den geschützten Code. Festgefahrene Rollouts und Experimente benötigen eine Entscheidung des Besitzers, bevor Code geändert wird. Fülle `templates/cleanup-ticket.md` pro Flag aus. ### 5. Liefern Gib das Urteil zurück, den priorisierten Plan mit Besitzern und file:line-Listen, die Tickets und zwei oder drei Richtlinienvorschläge (Ablaufdaten für jedes temporäre Flag, ein monatlicher Cleanup-Slot, eine Lint-Regel oder ein CI-Check, der bei unbekannten Schlüsseln fehlschlägt). ## Beispielergebnis Drei hohe Findings: `new-checkout-flow-v3` wird in checkout.ts gelesen, existiert aber nicht, `recs_carousel` in search.py ist ein Tippfehler von `recs-carousel` - das Karussell, das seit 170 Tagen "100% an" ist, wurde also nie angezeigt - und das archivierte `legacy-pdf-invoices` wird noch geprüft. `new-checkout-flow` ist seit 86 Tagen zu 100% an und im September abgelaufen (2 Referenzen, keep ON), das 2025er-Feiertagsbanner ist seit 303 Tagen aus (keep OFF), `search-v2` steckt seit 63 Tagen bei 25% fest, und das Preisexperiment läuft seit 199 Tagen. `dark-mode` ist in Produktion an, aber in Staging aus. Kill-Switch- und Berechtigungs-Flags werden behalten. ## Regeln - Empfiehl niemals, einen Kill-Switch, ein Ops- oder Berechtigungs-Flag zu entfernen, weil es alt ist; sie sind von Natur aus langlebig. - Ein Flag ohne Referenzen in den gescannten Ordnern kann woanders verwendet werden (Mobile-Apps, andere Dienste, Konfiguration); sag das, bevor du vorschlägst, es zu archivieren. - Entferne zuerst Code, deploye, archiviere dann das Flag im Service - niemals umgekehrt. - Ein festgefahrener Rollout oder ein laufendes Experiment braucht die Entscheidung des Besitzers; wähle nicht den Gewinner für sie. - Behandle `today` und das Stale-Fenster als Eingaben; sag, welche Werte du verwendet hast. FILE:references/flag-lifecycle-and-removal.md # Flag-Lebenszyklus und sicheres Entfernen ## Flag-Typen und erwartete Lebensdauer | Typ | Zweck | Erwartete Lebensdauer | Fertig, wenn | |---|---|---|---| | release | unfertige Arbeit verbergen, schrittweise ausrollen | Tage bis wenige Wochen nach 100% | 100% überall und stabil für das Stale-Fenster | | experiment | A/B- oder multivariater Test | die Testdauer (2-6 Wochen typisch) | eine Entscheidung getroffen ist | | ops | betrieblicher Schalter (Cache an/aus, Batch-Größe) | lang | selten; jährlich überprüfen | | kill-switch | eine riskante Abhängigkeit schnell abschalten | dauerhaft | nie nach Alter | | permission | Berechtigung, Beta-Zugang, Plan-Funktion | dauerhaft (oder ins Abrechnungs-/Berechtigungssystem verschieben) | Produktentscheidung | ## Ein Flag sicher entfernen 1. Bestätige den Zustand mit dem Besitzer: vollständig an (keep ON) oder vollständig aus (keep OFF), in jeder Umgebung und jedem Dienst, der es liest. 2. Finde alle Referenzen (die Liste des Scanners plus `git grep -n "<key>"` in anderen Repos, Mobile-Apps, Infrastruktur-Konfiguration, Analytics-Dashboards und Tests). 3. Keep ON: Ersetze den Check durch den ON-Zweig, lösche den OFF-Zweig, dann lösche Code, den nur der OFF-Zweig verwendet hat (alte Komponenten, Endpunkte, Styles, Migrations-Guards). Keep OFF: Lösche den geschützten Code und alles, was nur er verwendet hat. 4. Lösche flag-spezifische Tests und Fixtures; behalte Tests des Verhaltens, das bleibt. 5. Deploye in jede Umgebung. Mobile-Apps: Warte, bis alte App-Versionen, die das Flag lesen, unter deiner Support-Schwelle liegen. 6. Archiviere dann das Flag im Flag-Service (archiviere, lösche nicht, damit die Historie bleibt). 7. Schließe das Ticket mit den PR-Links. ## Häufige Fallstricke - Tippfehler und umbenannte Schlüssel: Das SDK gibt den Standardwert für unbekannte Schlüssel ohne Fehler zurück, sodass Code, der `recs_carousel` liest, nie `recs-carousel` sieht. Ein CI-Check, der Literalschlüssel mit dem Inventar vergleicht, fängt das ab. - Zuerst archivieren: Ein archiviertes Flag gibt den Standardwert überall zurück, wo es noch gelesen wird - oft den OFF-Pfad, also ein sofortiger Rollback einer gestarteten Funktion. - Festgefahrene Rollouts: 25% für zwei Monate bedeutet zwei Produkte zu unterstützen und zu testen; abschließen oder zurückrollen. - Experimente ohne Enddatum verzerren weiterhin Metriken und Analytics-Ereignisse. - Schlüssel, die zur Laufzeit erstellt werden (`"export-format-" + region`), können von der Suche nicht gefunden werden; behalte eine Konstantendatei, die jeden möglichen Schlüssel auflistet. ## Inventar exportieren - LaunchDarkly: REST `GET /api/v2/flags/{projectKey}?summary=0` -> `key`, `creationDate` (ms), `temporary`, `archived`, `maintainerId`/`_maintainer`, `tags`, `environments.<env>.on`, `.lastModified`, Rollout-Gewichte in `fallthrough.rollout.variations` (weight/1000 = Prozent). Mappe `temporary: false` auf `permanent: true`. - Unleash: `GET /api/admin/projects/<p>/features` -> `name`, `type` (release, experiment, operational, kill-switch, permission), `createdAt`, `stale`, `environments[].enabled`, schrittweise Rollout-Strategie `parameters.rollout`. - Flagsmith: `/api/v1/projects/<id>/features/` + pro Umgebung `featurestates` (`enabled`, `updated_at`), Prozentsatz über Segment-Overrides. - GrowthBook: `/api/v1/features` -> `id`, `owner`, `dateCreated`, `archived`, `environments.<env>.enabled`, Rollout-Regel `coverage` (0-1 -> Prozent). - ConfigCat: Management-API-Einstellungen + Werte pro Umgebung; Prozentoptionen geben den Rollout. - Hausinterne Konfiguration (YAML/JSON): ein Eintrag pro Flag; füge created/owner/expires manuell aus git blame hinzu. ## Richtlinien, die Flag-Schulden niedrig halten - Jedes temporäre Flag bekommt einen Besitzer, ein Ticket und ein Ablaufdatum, wenn es erstellt wird. - Cleanup ist Teil der Definition of Done: Das Entfernungs-Ticket wird mit dem Flag erstellt. - Ein monatlicher 30-minütiger Flag-Review mit diesem Audit; ein Limit für aktive Release-Flags pro Team. - CI: Fehlschlagen bei Schlüsseln, die nicht im Inventar sind; Warnen bei abgelaufenen Flags, die noch referenziert werden. FILE:references/checks-and-codes.md # Checks und Finding-Codes | Code | Schwere | Check | Was zu tun ist | |---|---|---|---| | FF01 | hoch | ein Flag-Check-Aufruf (isEnabled, variation, useFlag, is_enabled ...) verwendet einen Schlüssel, der nicht im Inventar ist; eine nahe Übereinstimmung wird vorgeschlagen (Tippfehler, `_` vs `-`) | korrigiere den Schlüssel oder erstelle das Flag; bis dahin gibt das SDK den Standardwert zurück | | FF02 | mittel | Release-Flag bei 100% in der primären Umgebung für `stale_days` oder mehr | entferne das Flag, behalte den ON-Pfad | | FF03 | mittel | Release- oder Experiment-Flag aus / 0% in der primären Umgebung für `stale_days` oder mehr | entferne das Flag und den geschützten Code oder entscheide dich zu starten | | FF04 | niedrig | keine Referenz in den gescannten Dateien | prüfe andere Repos und Dienste; archiviere, wenn ungenutzt | | FF05 | mittel | `expires`-Datum überschritten (temporäre Flags) | entferne oder vereinbare ein neues Datum | | FF06 | info | ops-, kill-switch-, permission- (oder `permanent: true`) Flag - nicht als veraltet beurteilt | jährlich überprüfen | | FF07 | niedrig | kein Besitzer | einen zuweisen | | FF08 | mittel | primäre Umgebung vor staging/qa/test/dev oder kein Zustand für die primäre Umgebung | Umgebungen angleichen | | FF09 | mittel | Experiment älter als `experiment_max_days` | entscheide, verschiffe den Gewinner, entferne | | FF10 | info | Flag-Schlüssel zur Laufzeit erstellt (Variable, Verkettung, Template-String) | verwende Literalschlüssel oder eine Konstantendatei | | FF11 | niedrig | temporäres Flag ohne `expires` | setze ein Ablaufdatum | | FF12 | mittel | Release-Rollout zwischen 1 und 99% unverändert für `stale_days` | schließe ab (100%) oder rolle zurück, dann entferne | | FF13 | mittel | Schlüssel zweimal im Inventar definiert | zusammenführen | | FF14 | hoch | Flag im Service archiviert, aber noch im Code referenziert | entferne die Referenzen | ## Urteil - **N hohe(s) Finding(s)**, wenn FF01/FF14 existieren; sonst **N Flag(s) bereit zum Entfernen**; sonst **keine veralteten Flags**. ## Planaktionen (in dieser Reihenfolge) Code entfernen (archiviert) -> entfernen, keep ON -> entfernen, keep OFF -> Rollout abschließen -> Experiment entscheiden -> archivieren (keine Referenzen). Innerhalb einer Aktion zuerst Flags mit mehr Referenzen. Aufwand: S 1-2 Referenzen, M 3-6, L 7+. ## Wie Referenzen gefunden werden 1. Aufrufe: `<name>(` mit einem Literal-Erstargument (oder einem Literal nach einem führenden Argument wie einem Client oder Benutzer), für die eingebauten Namen plus `call_names` / `--call`. 2. Jedes String-Literal gleich einem bekannten Schlüssel (Konstantendateien, Konfigurations-Maps). 3. `flags.`, `features.`, `featureFlags.`, `toggles.` gefolgt vom Schlüssel oder seiner camelCase-Form (`new-search` -> `flags.newSearch`). Kommentare, die den Schlüssel in Anführungszeichen enthalten, werden auch gezählt - prüfe die aufgelisteten Zeilen. ## Flags-JSON Oberste Ebene: `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` standardmäßig 100 bei an und 0 bei aus; `updated` ist die letzte Änderung dieser Umgebung (fällt auf `created` zurück). FILE:templates/cleanup-ticket.md # Feature-Flag `<key>` entfernen **Besitzer:** <team-payments> **Typ:** <release> **Ticket:** <PAY-412> **Aktion:** <entfernen, keep ON> **Zustand:** <100% an in Produktion seit 2026-08-11 (86 Tage), 100% in Staging> **Abgelaufen:** <2026-09-30> ## Warum <Vollständig ausgerollt und stabil seit 86 Tagen; der OFF-Pfad (Legacy-Checkout) ist toter Code.> ## Änderung - [ ] `<examples/src/checkout.ts:4>` - ersetze den Check durch den ON-Zweig (`renderOnePageCheckout`) - [ ] `<examples/src/checkout.ts:14>` - verwende immer `oneClickButton()` - [ ] lösche Code, den nur der OFF-Pfad verwendet hat: <renderLegacyCheckout, classicButton, Legacy-Checkout-Styles> - [ ] lösche flag-spezifische Tests und Fixtures - [ ] suche in anderen Repos / Mobile-Apps: `git grep -n "<key>"` ## Rollout 1. Merge und deploye nach Staging, dann Produktion. 2. Beobachte <Checkout-Konversion, Fehlerrate> für <2 Tage>. 3. Archiviere `<key>` in <Flag-Service> (archiviere, lösche nicht). ## Risiken / Fragen - <Liest die Mobile-App dieses Flag? Mindestversion unterstützt?> - <Gibt es Dashboards oder Alerts, die auf das Flag ausgerichtet sind?> 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": "Neuer einseitiger Checkout", "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": "Typesense-gestützte Suche", "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": "Weihnachts-Promo-Banner 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": "A/B-Test: Jahresplan zuerst", "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": "Dunkles Design", "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": "Kartenzahlungen während eines PSP-Ausfalls abschalten", "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": "CSV-Export für Beta-Kunden", "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": "Alter PDF-Renderer", "environments": {"production": {"on": false, "rollout": 0, "updated": "2026-02-01"}}}, {"key": "recs-carousel", "type": "release", "owner": "team-growth", "created": "2026-03-03", "description": "Empfehlungs-Karussell auf Produktseiten", "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("Kartenzahlungen sind pausiert. Bitte versuchen Sie es in Kürze erneut."); } const oneClick = await flags.isEnabled("new-checkout-flow", user); return oneClick ? oneClickButton() : classicButton(); } export async function invoiceLink(order: Order) { // TODO nach PDF-Migration entfernen 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="Weihnachts-Sale: 20% auf alles" />} <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("Export ist in der Beta") # regionale Experiment-Schlüssel, z. B. "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 # Feature-Flag-Cleanup-Audit Heute 2026-11-05, primäre Umgebung `production`, veraltet nach 30 Tagen. 9 Flag(s) im Inventar, 3 Quelldatei(en) in examples/src gescannt. **Urteil: 3 hohe(s) Finding(s) - Flags im Code verwendet, die der Flag-Service nicht kennt** Cleanup-Kandidaten: 6 (4 jetzt entfernbar, 4 Referenz(en) zu ändern). Findings: 3 hoch, 9 mittel, 2 niedrig, 3 info. ## Cleanup-Plan | # | Flag | Typ | Besitzer | Primär | Aktion | Warum | Refs (Dateien) | Aufwand | Wo | |---|---|---|---|---|---|---|---|---|---| | 1 | `legacy-pdf-invoices` | release | team-billing | archiviert | **Code entfernen** | archiviert, aber noch im Code | 1 (1) | S | `examples/src/checkout.ts:20` | | 2 | `new-checkout-flow` | release | team-payments | 100% | **entfernen, keep ON** | 100% an seit 86 Tagen | 2 (1) | S | `examples/src/checkout.ts:4`, `examples/src/checkout.ts:14` | | 3 | `recs-carousel` | release | team-growth | 100% | **entfernen, keep ON** | 100% an seit 170 Tagen | 0 (0) | - | - | | 4 | `holiday-banner-2025` | release | team-growth | 0% | **entfernen, keep OFF** | aus seit 303 Tagen | 1 (1) | S | `examples/src/Header.tsx:5` | | 5 | `search-v2` | release | team-search | 25% | **Rollout abschließen** | bei 25% festgefahren | 1 (1) | S | `examples/src/search.py:5` | | 6 | `pricing-page-test` | experiment | team-growth | 50% | **Experiment entscheiden** | läuft seit 199 Tagen | 1 (1) | S | `examples/src/Header.tsx:15` | Aufwand: S = 1-2 Referenzen, M = 3-6, L = 7 oder mehr. "keep ON" = den Flag-Check und den OFF-Zweig löschen; "keep OFF" = den geschützten Code löschen. Archiviere das Flag im Flag-Service erst, nachdem die Codeänderung überall deployed wurde. ## Findings | Sev | Code | Flag | Finding | Fix | |---|---|---|---|---| | hoch | FF01 | `new-checkout-flow-v3` | im Code verwendet (examples/src/checkout.ts:27), aber nicht im Flag-Inventar definiert - das SDK gibt den Standardwert zurück, dieser Codepfad liest also den falschen Schlüssel | Tippfehler von new-checkout-flow? verwende den exakten Schlüssel | | hoch | FF01 | `recs_carousel` | im Code verwendet (examples/src/search.py:20), aber nicht im Flag-Inventar definiert - das SDK gibt den Standardwert zurück, dieser Codepfad liest also den falschen Schlüssel | Tippfehler von recs-carousel? verwende den exakten Schlüssel | | hoch | FF14 | `legacy-pdf-invoices` | Flag ist im Flag-Service archiviert, wird aber noch an 1 Stelle(n) referenziert - es gibt jetzt den Standardwert zurück | entferne die Referenzen | | mittel | FF02 | `new-checkout-flow` | Release-Flag seit 86 Tagen zu 100% an in Produktion | entferne das Flag und behalte den ON-Pfad; lösche den OFF-Zweig und alten Code | | mittel | FF02 | `recs-carousel` | Release-Flag seit 170 Tagen zu 100% an in Produktion | entferne das Flag und behalte den ON-Pfad; lösche den OFF-Zweig und alten Code | | mittel | FF03 | `holiday-banner-2025` | Release-Flag seit 303 Tagen aus in Produktion - der geschützte Code ist tot | entferne das Flag und den geschützten Code (oder entscheide dich zu starten) | | mittel | FF05 | `holiday-banner-2025` | abgelaufen am 2026-01-10 (vor 299 Tagen) | entferne es jetzt oder vereinbare ein neues Datum mit dem Besitzer | | mittel | FF05 | `new-checkout-flow` | abgelaufen am 2026-09-30 (vor 36 Tagen) | entferne es jetzt oder vereinbare ein neues Datum mit dem Besitzer | | mittel | FF05 | `pricing-page-test` | abgelaufen am 2026-07-01 (vor 127 Tagen) | entferne es jetzt oder vereinbare ein neues Datum mit dem Besitzer | | mittel | FF08 | `dark-mode` | Produktion ist bei 100%, aber Staging bei 0% - Produktion läuft einen Pfad, den Staging nicht testet | Staging an Produktion angleichen | | mittel | FF09 | `pricing-page-test` | Experiment läuft seit 199 Tagen (Limit 90) | rufe das Ergebnis ab, verschiffe den Gewinner, entferne das Flag | | mittel | FF12 | `search-v2` | Rollout seit 63 Tagen bei 25% in Produktion festgefahren - Nutzer bekommen zwei Versionen | schließe den Rollout ab (100%) oder rolle auf 0 zurück, dann entferne | | niedrig | FF04 | `recs-carousel` | keine Referenzen in 3 gescannten Datei(en) | prüfe andere Repos/Dienste; wenn ungenutzt, archiviere es im Flag-Service | | niedrig | FF07 | `beta-reports-export` | kein Besitzer | weise ein Besitzer-Team oder eine Person zu | | info | FF06 | `beta-reports-export` | permission-Flag - von Natur aus langlebig, nicht auf Veralterung geprüft | jährlich überprüfen; Beschreibung und Besitzer behalten | | info | FF06 | `payments-kill-switch` | kill-switch-Flag - von Natur aus langlebig, nicht auf Veralterung geprüft | jährlich überprüfen; Beschreibung und Besitzer behalten | | info | FF10 | `-` | Flag-Schlüssel zur Laufzeit erstellt bei examples/src/search.py:14 - Referenzen darauf können von der Suche nicht gefunden werden | bevorzuge Literalschlüssel (oder eine Konstantendatei), damit Flags aufgeräumt werden können | ## Referenzen pro Flag | Flag | Referenzen | |---|---| | `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 | Die Suche deckt String-Literale, gängige SDK-Aufrufe und `flags.camelCase`-Eigenschaften nur in den gescannten Ordnern ab; prüfe andere Dienste, Mobile-Apps und Konfigurations-Repos, bevor du ein Flag archivierst. FILE:scripts/flag_audit.py #!/usr/bin/env python3 """Feature-Flag-Stale-Cleanup-Auditor (nur Standardbibliothek, Python 3.8+). Liest ein Feature-Flag-Inventar (JSON exportiert oder von Hand geschrieben aus LaunchDarkly, Unleash, Flagsmith, GrowthBook, ConfigCat oder einer hausinternen Konfiguration) und scannt einen Quellbaum nach Flag- Referenzen. Findet Release-Flags, die seit Wochen vollständig an (oder aus) sind und entfernt werden können, teilweise Rollouts, die festgefahren sind, abgelaufene und verwaiste Flags, Experimente, die nie endeten, Flags im Code verwendet, die aber im Flag-Service fehlen (sie fallen stillschweigend auf den Standardwert zurück), Umgebungsdrift und Flags ohne Besitzer - und schreibt einen priorisierten Cleanup-Plan mit den exakten file:line-Referenzen, die zu ändern sind. Verwendung: flag_audit.py FLAGS.json SRC_DIR [SRC_DIR ...] [--today YYYY-MM-DD] [--stale-days N] [--call NAME ...] [--json] flag_audit.py --self-test Exit: 0 = keine hohen Findings, 1 = mindestens ein hohes Finding, 2 = ungültige Eingabe. """ 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 muss YYYY-MM-DD sein, erhalten %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("Flags-Datei muss ein JSON-Objekt mit einer \"flags\"-Liste sein") bad = set(cfg) - TOP_KEYS if bad: raise InputError("unbekannte(r) Schlüssel auf oberster Ebene: %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 hat keinen Schlüssel" % i) badk = set(f) - FLAG_KEYS if badk: raise InputError("Flag %s: unbekannte(r) Schlüssel %s" % (f["key"], ", ".join(sorted(badk)))) t = (f.get("type") or "release").lower() if t not in TYPES: raise InputError("Flag %s: Typ muss einer von %s sein, erhalten %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 Umgebung %s muss ein Objekt {on, rollout, updated} sein" % (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 Umgebung %s: rollout muss 0-100 sein, erhalten %r" % (f["key"], e, r)) envs[e] = {"on": bool(st.get("on")), "rollout": float(r), "updated": pdate(st["updated"], "Flag %s Umgebung %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): """effektiver Anteil der Nutzer, die das Flag bekommen: 0-100""" return st["rollout"] if st["on"] else 0.0 # ---------------------------------------------------------------- scan def scan(dirs, keys, calls): """Gibt refs {key: [(pfad, zeile, text)]}, unknown {literal: [(pfad,zeile)]}, dynamic [(pfad,zeile,text)], dateien zurück""" 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("Quellverzeichnis nicht gefunden: %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("kann %s nicht lesen: %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 zweimal im Inventar definiert (das letzte wird verwendet)", "führe die zwei Einträge zusammen") 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, "im Code verwendet (%s), aber nicht im Flag-Inventar definiert - das SDK gibt den Standardwert zurück, dieser Codepfad ist also %s" % (locs, "wahrscheinlich nie an" if not close else "liest den falschen Schlüssel"), ("Tippfehler von %s? verwende den exakten Schlüssel" % close) if close else "erstelle das Flag oder entferne den Check") for p, i, txt in dynamic[:20]: add("info", "FF10", "-", "Flag-Schlüssel zur Laufzeit erstellt bei %s:%d - Referenzen darauf können von der Suche nicht gefunden werden" % (p, i), "bevorzuge Literalschlüssel (oder eine Konstantendatei), damit Flags aufgeräumt werden können") 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, "kein Besitzer", "weise ein Besitzer-Team oder eine Person zu") if f["archived"]: if r: add("high", "FF14", k, "Flag ist im Flag-Service archiviert, wird aber noch an %d Stelle(n) referenziert - es gibt jetzt den Standardwert zurück" % len(r), "entferne die Referenzen") plan.append({"key": k, "type": f["type"], "owner": f["owner"] or "?", "state": "archiviert", "action": "Code entfernen", "why": "archiviert, aber noch im 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, "kein Zustand für die primäre Umgebung %r" % prim, "füge den %s-Zustand hinzu oder korrigiere primary_env" % prim) if f["permanent"]: add("info", "FF06", k, "%s-Flag - von Natur aus langlebig, nicht auf Veralterung geprüft" % f["type"], "jährlich überprüfen; Beschreibung und Besitzer behalten") if not r: add("low", "FF04", k, "keine Referenzen in %d gescannten Datei(en)" % nfiles, "prüfe andere Repos/Dienste; wenn ungenutzt, archiviere es im Flag-Service") action, why = action or "archivieren", "keine Referenzen" 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, "abgelaufen am %s (vor %d Tagen)" % (f["expires"], (today - f["expires"]).days), "entferne es jetzt oder vereinbare ein neues Datum mit dem Besitzer") if f["type"] == "release" and e == 100 and days is not None and days >= stale: add("medium", "FF02", k, "Release-Flag seit %d Tagen zu 100%% an in %s%s" % (days, prim, "" if all_on else " (nicht 100% in jeder Umgebung)"), "entferne das Flag und behalte den ON-Pfad; lösche den OFF-Zweig und alten Code") action, why = "entfernen, keep ON", "100%% an seit %d Tagen" % days elif e == 0 and days is not None and days >= stale and f["type"] in ("release", "experiment"): add("medium", "FF03", k, "%s-Flag seit %d Tagen aus in %s - der geschützte Code ist tot" % (f["type"], days, prim), "entferne das Flag und den geschützten Code (oder entscheide dich zu starten)") action, why = "entfernen, keep OFF", "aus seit %d Tagen" % days elif 0 < e < 100 and days is not None and days >= stale and f["type"] == "release": add("medium", "FF12", k, "Rollout seit %d Tagen bei %g%% in %s festgefahren - Nutzer bekommen zwei Versionen" % (days, e, prim), "schließe den Rollout ab (100%) oder rolle auf 0 zurück, dann entferne") action, why = "Rollout abschließen", "bei %g%% festgefahren" % e if f["type"] == "experiment" and age is not None and age > c["exp_max"]: add("medium", "FF09", k, "Experiment läuft seit %d Tagen (Limit %d)" % (age, c["exp_max"]), "rufe das Ergebnis ab, verschiffe den Gewinner, entferne das Flag") action, why = action or "Experiment entscheiden", "läuft seit %d Tagen" % age if not r: add("low", "FF04", k, "keine Referenzen in %d gescannten Datei(en)" % nfiles, "prüfe andere Repos/Dienste; wenn ungenutzt, archiviere es im Flag-Service") action, why = action or "archivieren", why or "keine Referenzen" 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 ist bei %g%%, aber %s bei %g%% - Produktion läuft einen Pfad, den Staging nicht testet" % (prim, e, en, eff(s2)), "gleiche %s an %s an" % (en, prim)) if f["expires"] is None and f["type"] in ("release", "experiment") and action is None: add("low", "FF11", k, "temporäres %s-Flag ohne Ablaufdatum" % f["type"], "setze expires (z. B. 30-60 Tage nach vollem Rollout)") 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 = {"Code entfernen": 0, "entfernen, keep ON": 1, "entfernen, keep OFF": 2, "Rollout abschließen": 3, "Experiment entscheiden": 4, "archivieren": 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("entfernen")] out = ["# Feature-Flag-Cleanup-Audit", "", "Heute %s, primäre Umgebung `%s`, veraltet nach %d Tagen. %d Flag(s) im Inventar, %d Quelldatei(en) in %s gescannt." % ( r["today"], r["primary"], r["stale_days"], r["flags"], r["files"], ", ".join(src)), ""] if c["high"]: v = "%d hohe(s) Finding(s) - Flags im Code verwendet, die der Flag-Service nicht kennt" % c["high"] elif removable: v = "%d Flag(s) bereit zum Entfernen" % len(removable) else: v = "keine veralteten Flags" out += ["**Urteil: %s**" % v, "", "Cleanup-Kandidaten: %d (%d jetzt entfernbar, %d Referenz(en) zu ändern). Findings: %d hoch, %d mittel, %d niedrig, %d info." % ( len(r["plan"]), len(removable), sum(p["refs"] for p in removable), c["high"], c["medium"], c["low"], c["info"]), ""] out += ["## Cleanup-Plan", "", "| # | Flag | Typ | Besitzer | Primär | Aktion | Warum | Refs (Dateien) | Aufwand | Wo |", "|---|---|---|---|---|---|---|---|---|---|"] for i, p in enumerate(r["plan"], 1): locs = ", ".join("`%s`" % x for x in p["locs"][:4]) + (" +%d mehr" % (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 += ["", "Aufwand: S = 1-2 Referenzen, M = 3-6, L = 7 oder mehr. \"keep ON\" = den Flag-Check und den OFF-Zweig löschen; \"keep OFF\" = den geschützten Code löschen. Archiviere das Flag im Flag-Service erst, nachdem die Codeänderung überall deployed wurde.", ""] out += ["## Findings", "", "| Sev | Code | Flag | Finding | Fix |", "|---|---|---|---|---|"] for s, code, k, msg, fix in r["findings"]: out.append("| %s | %s | `%s` | %s | %s |" % (s, code, k, msg.replace("|", "\\|"), fix.replace("|", "\\|"))) out += ["", "## Referenzen pro Flag", "", "| Flag | Referenzen |", "|---|---|"] for k, n in sorted(r["refs"].items()): out.append("| `%s` | %d |" % (k, n)) out += ["", "Die Suche deckt String-Literale, gängige SDK-Aufrufe und `flags.camelCase`-Eigenschaften nur in den gescannten Ordnern ab; prüfe andere Dienste, Mobile-Apps und Konfigurations-Repos, bevor du ein Flag archivierst."] 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("kann %s nicht lesen: %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% an seit 65 Tagen -> FF02", ("medium", "FF02", "new-checkout") in codes(r)) t("100% an seit 6 Tagen -> nicht veraltet", ("medium", "FF02", "dark-mode") not in codes(r)) t("Planaktion keep ON", [p["action"] for p in r["plan"] if p["key"] == "new-checkout"] == ["entfernen, keep ON"]) t("Refs über Aufruf gefunden", 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("aus seit 65 Tagen -> FF03 keep OFF", ("medium", "FF03", "old-banner") in codes(r) and r["plan"][0]["action"] == "entfernen, keep OFF") r = mk([rel("half", r=50)], {"c.js": "isEnabled('half')"}) t("festgefahrener Rollout -> FF12", ("medium", "FF12", "half") in codes(r)) r = mk([rel("new-checkout")], {"a.ts": "isEnabled('new_checkout')\n"}) t("undefinierter Schlüssel im Code -> FF01 hoch", ("high", "FF01", "new_checkout") in codes(r)) t("FF01 schlägt den nahen Schlüssel vor", any("Tippfehler von new-checkout" in f[4] for f in r["findings"])) t("FF01 allein bedeutet 0 Refs für den echten Schlüssel -> FF04", ("low", "FF04", "new-checkout") in codes(r)) r = mk([rel("ghost")], {"a.ts": "const a = 1\n"}) t("keine Refs -> FF04 + archivieren", ("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("Experiment über 90 Tage -> FF09", ("medium", "FF09", "exp1") in codes(r)) r = mk([rel("exp2", expires="2026-10-01", up="2026-10-30")], {"a.ts": "isEnabled('exp2')"}) t("abgelaufen -> FF05", ("medium", "FF05", "exp2") in codes(r)) r = mk([dict(rel("ks"), type="kill-switch", expires=None)], {"a.ts": "isEnabled('ks')"}) t("kill-switch übersprungen -> nur 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("kein Besitzer -> 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("Produktion vor Staging -> FF08", ("medium", "FF08", "d") in codes(r)) r = mk([rel("x")], {"a.ts": "const k = 'x' + region\nisEnabled(flagName)\n"}) t("dynamischer Schlüssel -> FF10 info", ("info", "FF10", "-") in codes(r)) r = mk([rel("x")], {"a.py": "is_enabled('export-' + region)\n"}) t("Literal + Variable ist dynamisch, nicht undefiniert", ("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("flags.camelCase-Eigenschaft gezählt", 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 und Nicht-Code-Dateien übersprungen", r["refs"]["new-search"] == 0) r = mk([rel("t", up="2026-10-30", expires=None)], {"a.ts": "isEnabled('t')"}) t("kein Ablauf -> FF11", ("low", "FF11", "t") in codes(r)) r = mk([rel("arch", archived=True)], {"a.ts": "isEnabled('arch')"}) t("archiviert, aber referenziert -> FF14 hoch", ("high", "FF14", "arch") in codes(r) and r["plan"][0]["action"] == "Code entfernen") r = mk([rel("dup", up="2026-10-30"), rel("dup", up="2026-10-30")], {"a.ts": "isEnabled('dup')"}) t("doppelter Schlüssel -> 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("benutzerdefinierter Aufrufname findet undefinierte Schlüssel", ("high", "FF01", "nope") in codes(r)) r = mk([rel("stale14", up="2026-10-20")], {"a.ts": "isEnabled('stale14')"}, stale_days=14) t("stale_days-Einstellung respektiert", ("medium", "FF02", "stale14") in codes(r)) for bad, name in (({"flags": [{"key": "a", "type": "temp"}]}, "schlechter Typ"), ({"flags": [{"key": "a", "environments": {"production": {"on": True, "rollout": 150}}}]}, "rollout > 100"), ({"flags": [{"key": "a", "colour": 1}]}, "unbekannter Flag-Schlüssel"), ({"flag": []}, "fehlende flags-Liste")): try: load_flags(bad); t("abgelehnt: " + name, False) except InputError: t("abgelehnt: " + name, True) r = mk([rel("new-checkout")], src) t("Bericht hat Urteil und Plan", "**Urteil:" in report(r, ["x"]) and "## Cleanup-Plan" 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 bestanden" % (p, len(tests))) return 0 if p == len(tests) else 1 def main(argv=None): ap = argparse.ArgumentParser(description="Veraltete Feature-Flags finden und ihre Entfernung planen.") 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="zusätzlicher Flag-Check-Funktionsname (wiederholbar)") 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("gib FLAGS.json und mindestens ein Quellverzeichnis an") try: r = run(a.flags, a.src, a.today, a.stale_days, a.call) except InputError as e: print("Fehler: %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())

Melde dich an, um den vollständigen Prompt zu sehen

Weiter mit:

Mit der Anmeldung akzeptierst du unsere Nutzungsbedingungen und Datenschutz

Verwendung

Dieser Prompt ist für die Verwendung mit coding gedacht. Kopiere den Inhalt oben und füge ihn in dein bevorzugtes KI-Tool ein.

Für beste Ergebnisse passe die Platzhalter (eckige Klammern oder Großbuchstaben) an deine Anforderungen an.

Referenzen

Kategorien:coding| prompts.chat| feature-flags| code-cleanup

Diskussion