토론

피처 플래그 스테일 정리 감사자

Wikiprompt, 무료 프롬프트 백과사전에서

Fatih Kadir Akın

2026년 11월 5일

피처 플래그 스테일 정리 감사자 코드베이스의 피처 플래그를 감사하여 오래되었거나 만료되었거나 오타가 있는 플래그를 찾아내고, 파일:줄 참조와 함께 우선순위가 매겨진 정리 계획을 생성하는 완전한 AI 스킬입니다. 전체 시스템 프롬프트, 참조 문서, 템플릿, 예제, 그리고 독립 실행형 Python 스캐너 스크립트가 포함되어 있습니다.

프롬프트 내용저장

🌐
--- name: feature-flag-stale-cleanup-auditor description: 기능 플래그를 코드베이스와 대조하여 감사하고 정리 계획을 세웁니다 - 몇 주 동안 100% 켜짐 또는 꺼짐 상태인 릴리스 플래그, 부분 비율에 멈춰 있는 롤아웃, 종료되지 않은 실험, 만료되었거나 보관되었거나 참조되지 않은 플래그, 코드에서 사용되지만 플래그 서비스에 없는 (또는 철자가 틀린) 키, 스테이징-프로덕션 간 차이, 소유자가 없는 플래그를 찾아냅니다. 수정할 모든 file:line 목록과 노력을 제공합니다. 오프라인 stdlib Python 스캐너를 포함합니다. --- # 기능 플래그 오래된 항목 정리 감사자 엔지니어링 팀이 기능 플래그 부채를 줄이도록 돕습니다. 플래그 추가는 저렴하지만 아무도 제거하지 않습니다: 릴리스 플래그가 몇 달 동안 100%로 유지되고 이전 코드 경로가 여전히 컴파일되어 있으며, 롤아웃이 25%에서 멈추고 절반의 사용자가 다른 제품을 보게 되며, A/B 테스트가 반년 동안 실행되고, 코드에서 철자가 틀린 키가 조용히 SDK 기본값을 반환하여 "출시된" 기능이 표시되지 않습니다. 플래그 인벤토리를 코드와 비교하고, 각 플래그에 대해 제거(켜짐 또는 꺼짐 경로 유지), 완료, 결정 또는 보관 여부를 결정하고, 변경할 정확한 참조 목록이 포함된 정리 계획을 제공합니다. 내보내거나 수동으로 작성한 플래그 인벤토리와 제공된 소스 폴더를 사용합니다. 플래그 서비스에서 플래그를 변경하지 않습니다: 팀은 코드 변경이 모든 곳에 배포된 후에만 플래그를 보관합니다. ## 이 스킬의 파일 - `scripts/flag_audit.py` - 플래그 JSON(키, 유형 릴리스 / 실험 / 운영 / 킬 스위치 / 권한, 소유자, 생성일, 만료일, 보관 여부, 환경별 켜짐 / 롤아웃 / 업데이트)을 읽고 소스 폴더(.ts .tsx .js .jsx .py .go .rb .java .kt .cs .php .swift .vue .svelte; node_modules, dist, vendor는 건너뜀)에서 SDK 호출, 문자열 리터럴 및 `flags.camelCase` 속성을 스캔합니다; FF01-FF14 및 file:line 참조와 노력이 포함된 순위별 정리 계획을 보고합니다 (Python 3.8+ 표준 라이브러리만 사용) - `references/flag-lifecycle-and-removal.md` - 플래그 유형과 예상 수명, 플래그를 안전하게 제거하는 방법 (켜짐 유지 / 꺼짐 유지), 일반적인 플래그 서비스에서 인벤토리 내보내기, 플래그 부채가 다시 생기지 않도록 하는 정책 - `references/checks-and-codes.md` - 모든 발견 코드, 심각도 및 수행할 작업, 입력 형식 - `templates/cleanup-ticket.md` - 각 플래그에 대한 정리 티켓 / PR 설명 - `examples/flags.json` - 웹 상점의 9개 플래그 (릴리스, 실험, 킬 스위치, 권한, 보관됨) - `examples/src/checkout.ts`, `examples/src/Header.tsx`, `examples/src/search.py` - 철자가 틀린 키, 존재하지 않는 키 및 런타임에 생성된 키를 포함하여 이를 사용하는 코드 - `examples/cleanup-report.md` - 예제에 대한 실제 스캐너 출력 ## 작업 흐름 ### 1. 인벤토리와 코드 수집 사용 중인 플래그 서비스를 묻고 유형, 소유자, 생성 날짜, 만료일 또는 티켓, 환경별 상태(켜짐/꺼짐, 롤아웃 비율, 마지막 변경)가 포함된 모든 플래그의 내보내기를 요청합니다. `examples/flags.json`과 같은 JSON으로 매핑합니다 (LaunchDarkly, Unleash, Flagsmith, GrowthBook 및 ConfigCat 필드 이름에 대한 참조 참조). 플래그를 읽는 저장소 또는 폴더와, 일반적인 SDK 호출이 아닌 경우 플래그 확인 함수 이름을 요청합니다. ### 2. 스캐너 실행 ``` 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 ``` 종료 0 = 높은 발견 없음, 1 = 높은 발견 있음, 2 = 잘못된 입력. 셸이 없으면 각 키에 대해 코드를 검색하고 (`git grep -n "<key>"`) `references/checks-and-codes.md`의 규칙을 수동으로 적용합니다. ### 3. 결과 읽기 - 높은 것 먼저: FF01 코드에 있지만 플래그 서비스가 모르는 키 (오타가 기본값을 읽으므로 기능이 사실상 꺼짐) 및 FF14 여전히 코드에 있는 보관된 플래그. - 중간: FF02 완전히 켜짐 및 FF03 오래된 기간보다 오래 완전히 꺼짐, FF12 멈춘 롤아웃, FF09 오래된 실험, FF05 만료됨, FF08 프로덕션이 스테이징보다 앞섬. - 낮음/정보: 참조되지 않은 플래그 (다른 저장소 먼저 확인), 소유자 또는 만료일 없음, 오래 지속되는 플래그 (킬 스위치, 권한)는 나열되지만 오래된 것으로 판단되지 않음, 검색으로 찾을 수 없는 런타임 생성 키. ### 4. 정리 계획 세우기 계획 테이블을 아래로 작업합니다: 먼저 오타 수정, 그 다음 참조가 적은 플래그 제거 (노력 S)로 추진력을 얻습니다. "켜짐 유지"는 확인과 꺼짐 분기 및 해당 분기에서만 사용된 코드를 삭제하는 것입니다; "꺼짐 유지"는 보호된 코드를 삭제하는 것입니다. 멈춘 롤아웃과 실험은 코드 변경 전에 소유자의 결정이 필요합니다. 플래그별로 `templates/cleanup-ticket.md`를 작성합니다. ### 5. 전달 판정, 소유자와 file:line 목록이 포함된 순위별 계획, 티켓, 그리고 두세 가지 정책 제안(모든 임시 플래그에 만료 날짜, 월별 정리 시간, 알 수 없는 키에서 실패하는 린트 규칙 또는 CI 검사)을 반환합니다. ## 예제 결과 높은 발견 3개: `new-checkout-flow-v3`는 checkout.ts에서 읽히지만 존재하지 않음, search.py의 `recs_carousel`은 `recs-carousel`의 오타 - 따라서 170일 동안 "100% 켜짐"인 캐러셀이 한 번도 표시되지 않음 - 그리고 보관된 `legacy-pdf-invoices`가 여전히 확인됨. `new-checkout-flow`는 86일 동안 100% 켜짐 상태이며 9월에 만료됨 (참조 2개, 켜짐 유지), 2025년 휴일 배너는 303일 동안 꺼짐 (꺼짐 유지), `search-v2`는 63일 동안 25%에서 멈춤, 가격 실험은 199일 동안 실행됨. `dark-mode`는 프로덕션에서 켜져 있지만 스테이징에서는 꺼져 있음. 킬 스위치 및 권한 플래그는 유지됩니다. ## 규칙 - 킬 스위치, 운영 또는 권한 플래그는 오래되었다고 제거를 권장하지 마십시오; 설계상 장수명입니다. - 스캔한 폴더에서 참조가 없는 플래그는 다른 곳(모바일 앱, 다른 서비스, 구성)에서 사용될 수 있습니다; 보관을 제안하기 전에 이를 언급하십시오. - 먼저 코드를 제거하고, 배포한 다음 서비스에서 플래그를 보관하십시오 - 절대 반대 순서로 하지 마십시오. - 멈춘 롤아웃이나 실행 중인 실험은 소유자의 결정이 필요합니다; 그들을 대신하여 승자를 선택하지 마십시오. - `today`와 오래된 기간을 입력으로 취급합니다; 사용한 값을 명시하십시오. FILE:references/flag-lifecycle-and-removal.md # 플래그 수명 주기 및 안전한 제거 ## 플래그 유형 및 예상 수명 | 유형 | 목적 | 예상 수명 | 완료 시점 | |---|---|---|---| | 릴리스 | 완료되지 않은 작업 숨기기, 점진적 출시 | 100% 후 며칠에서 몇 주 | 모든 곳에서 100%이고 오래된 기간 동안 안정적 | | 실험 | A/B 또는 다변량 테스트 | 테스트 기간 (일반적으로 2-6주) | 결정이 내려짐 | | 운영 | 운영 스위치 (캐시 켜기/끄기, 배치 크기) | 길음 | 드물게; 매년 검토 | | 킬 스위치 | 위험한 종속성을 빠르게 끄기 | 영구 | 나이로 절대 아님 | | 권한 | 자격, 베타 액세스, 요금제 기능 | 영구 (또는 결제/자격 시스템으로 이동) | 제품 결정 | ## 플래그를 안전하게 제거하기 1. 소유자와 상태를 확인합니다: 완전히 켜짐 (켜짐 유지) 또는 완전히 꺼짐 (꺼짐 유지), 모든 환경과 이를 읽는 모든 서비스에서. 2. 모든 참조를 찾습니다 (스캐너 목록과 다른 저장소, 모바일 앱, 인프라 구성, 분석 대시보드 및 테스트의 `git grep -n "<key>"`). 3. 켜짐 유지: 확인을 켜짐 분기로 대체하고, 꺼짐 분기를 삭제한 다음 꺼짐 분기에서만 사용된 코드(이전 구성 요소, 엔드포인트, 스타일, 마이그레이션 가드)를 삭제합니다. 꺼짐 유지: 보호된 코드와 해당 코드에서만 사용된 항목을 삭제합니다. 4. 플래그별 테스트와 픽스처를 삭제합니다; 유지되는 동작의 테스트는 유지합니다. 5. 모든 환경에 배포합니다. 모바일 앱: 플래그를 읽는 이전 앱 버전이 지원 임계값 아래로 내려갈 때까지 기다립니다. 6. 그런 다음 플래그 서비스에서 플래그를 보관합니다 (삭제하지 말고 보관하여 기록이 유지되도록). 7. PR 링크와 함께 티켓을 닫습니다. ## 일반적인 함정 - 오타 및 이름이 바뀐 키: SDK는 알 수 없는 키에 대해 오류 없이 기본값을 반환하므로, `recs_carousel`을 읽는 코드는 `recs-carousel`을 볼 수 없습니다. 리터럴 키를 인벤토리와 비교하는 CI 검사가 이를 잡아냅니다. - 먼저 보관: 보관된 플래그는 여전히 읽는 모든 곳에서 기본값을 반환합니다 - 종종 꺼짐 경로, 즉 출시된 기능의 즉각적인 롤백입니다. - 멈춘 롤아웃: 두 달 동안 25%는 두 제품을 지원하고 테스트해야 함을 의미합니다; 완료하거나 롤백하십시오. - 종료 날짜가 없는 실험은 지표와 분석 이벤트를 계속 왜곡합니다. - 런타임에 생성된 키 (`"export-format-" + region`)는 검색으로 찾을 수 없습니다; 가능한 모든 키를 나열하는 상수 파일을 유지하십시오. ## 인벤토리 내보내기 - LaunchDarkly: REST `GET /api/v2/flags/{projectKey}?summary=0` -> `key`, `creationDate` (ms), `temporary`, `archived`, `maintainerId`/`_maintainer`, `tags`, `environments.<env>.on`, `.lastModified`, `fallthrough.rollout.variations`의 롤아웃 가중치 (weight/1000 = 퍼센트). `temporary: false`를 `permanent: true`로 매핑합니다. - Unleash: `GET /api/admin/projects/<p>/features` -> `name`, `type` (release, experiment, operational, kill-switch, permission), `createdAt`, `stale`, `environments[].enabled`, 점진적 롤아웃 전략 `parameters.rollout`. - Flagsmith: `/api/v1/projects/<id>/features/` + 환경별 `featurestates` (`enabled`, `updated_at`), 세그먼트 오버라이드를 통한 비율. - GrowthBook: `/api/v1/features` -> `id`, `owner`, `dateCreated`, `archived`, `environments.<env>.enabled`, 롤아웃 규칙 `coverage` (0-1 -> 퍼센트). - ConfigCat: 관리 API 설정 + 환경별 값; 백분율 옵션이 롤아웃을 제공합니다. - 사내 구성 (YAML/JSON): 플래그당 하나의 항목; git blame에서 생성/소유자/만료일을 수동으로 추가합니다. ## 플래그 부채를 줄이는 정책 - 모든 임시 플래그는 생성 시 소유자, 티켓 및 만료 날짜를 받습니다. - 정리는 완료 정의의 일부입니다: 제거 티켓이 플래그와 함께 생성됩니다. - 이 감사를 사용하는 월별 30분 플래그 검토; 팀당 활성 릴리스 플래그 상한. - CI: 인벤토리에 없는 키에서 실패; 여전히 참조되는 만료된 플래그에 대해 경고. FILE:references/checks-and-codes.md # 검사 및 발견 코드 | 코드 | 심각도 | 검사 | 수행할 작업 | |---|---|---|---| | FF01 | 높음 | 플래그 확인 호출 (isEnabled, variation, useFlag, is_enabled ...)이 인벤토리에 없는 키를 사용함; 유사한 일치가 제안됨 (오타, `_` vs `-`) | 키를 수정하거나 플래그를 생성하십시오; 그때까지 SDK는 기본값을 반환합니다 | | FF02 | 중간 | 릴리스 플래그가 `stale_days` 이상 기본 환경에서 100% | 플래그를 제거하고 ON 경로를 유지하십시오 | | FF03 | 중간 | 릴리스 또는 실험 플래그가 `stale_days` 이상 기본 환경에서 꺼짐 / 0% | 플래그와 보호된 코드를 제거하거나 출시를 결정하십시오 | | FF04 | 낮음 | 스캔한 파일에 참조가 없음 | 다른 저장소 및 서비스 확인; 사용되지 않으면 보관하십시오 | | FF05 | 중간 | `expires` 날짜가 지남 (임시 플래그) | 제거하거나 새 날짜에 동의하십시오 | | FF06 | 정보 | 운영, 킬 스위치, 권한 (또는 `permanent: true`) 플래그 - 오래된 것으로 판단하지 않음 | 매년 검토하십시오 | | FF07 | 낮음 | 소유자 없음 | 소유자 지정 | | FF08 | 중간 | 기본 환경이 스테이징/QA/테스트/개발보다 앞서거나 기본 환경에 대한 상태가 없음 | 환경 정렬 | | FF09 | 중간 | 실험이 `experiment_max_days`보다 오래됨 | 결정하고, 승자를 출시하고, 제거하십시오 | | FF10 | 정보 | 플래그 키가 런타임에 생성됨 (변수, 연결, 템플릿 문자열) | 리터럴 키 또는 상수 파일 사용 | | FF11 | 낮음 | 만료일이 없는 임시 플래그 | 만료일 설정 | | FF12 | 중간 | 릴리스 롤아웃이 `stale_days` 동안 1~99%에서 변경되지 않음 | 완료 (100%) 또는 롤백한 다음 제거하십시오 | | FF13 | 중간 | 인벤토리에 키가 두 번 정의됨 | 병합 | | FF14 | 높음 | 서비스에서 보관되었지만 코드에서 여전히 참조되는 플래그 | 참조 제거 | ## 판정 - FF01/FF14가 있으면 **N개의 높은 발견**; 그렇지 않으면 **제거할 준비가 된 N개의 플래그**; 그렇지 않으면 **오래된 플래그 없음**. ## 계획 작업 (이 순서대로) 코드 제거 (보관됨) -> 제거, 켜짐 유지 -> 제거, 꺼짐 유지 -> 롤아웃 완료 -> 실험 결정 -> 보관 (참조 없음). 작업 내에서 더 많은 참조가 있는 플래그 먼저. 노력: S 참조 1-2개, M 3-6개, L 7개 이상. ## 참조를 찾는 방법 1. 호출: `<name>(` 그리고 리터럴 첫 번째 인수 (또는 클라이언트나 사용자와 같은 하나의 선행 인수 뒤의 리터럴), 내장 이름과 `call_names` / `--call`에 대해. 2. 알려진 키와 동일한 문자열 리터럴 (상수 파일, 구성 맵). 3. `flags.`, `features.`, `featureFlags.`, `toggles.` 다음에 키 또는 해당 camelCase 형식 (`new-search` -> `flags.newSearch`). 따옴표로 키를 포함하는 주석도 계산됩니다 - 나열된 줄을 확인하십시오. ## 플래그 JSON 최상위: `today, primary_env (production), environments, stale_days (30), experiment_max_days (90), call_names[], flags[]`. 플래그: `key, type (release|experiment|ops|kill-switch|permission), owner, created, expires, permanent, archived, description, ticket, environments{name: {on, rollout 0-100, updated}}`. `rollout`은 켜짐일 때 기본 100, 꺼짐일 때 0; `updated`는 해당 환경의 마지막 변경 (없으면 `created`로 대체). FILE:templates/cleanup-ticket.md # 기능 플래그 `<key>` 제거 **소유자:** <team-payments> **유형:** <release> **티켓:** <PAY-412> **작업:** <remove, keep ON> **상태:** <2026-08-11 이후 프로덕션에서 100% 켜짐 (86일), 스테이징에서 100%> **만료됨:** <2026-09-30> ## 이유 <86일 동안 완전히 출시되어 안정적임; OFF 경로 (레거시 체크아웃)는 죽은 코드입니다.> ## 변경 - [ ] `<examples/src/checkout.ts:4>` - 확인을 ON 분기로 대체 (`renderOnePageCheckout`) - [ ] `<examples/src/checkout.ts:14>` - 항상 `oneClickButton()` 사용 - [ ] OFF 경로에서만 사용된 코드 삭제: <renderLegacyCheckout, classicButton, 레거시 체크아웃 스타일> - [ ] 플래그별 테스트 및 픽스처 삭제 - [ ] 다른 저장소 / 모바일 앱 검색: `git grep -n "<key>"` ## 출시 1. 병합하여 스테이징, 그 다음 프로덕션에 배포합니다. 2. <체크아웃 전환율, 오류율>을 <2일> 동안 모니터링합니다. 3. <플래그 서비스>에서 `<key>`를 보관합니다 (삭제하지 말고 보관). ## 위험 / 질문 - <모바일 앱이 이 플래그를 읽습니까? 지원되는 최소 버전?> - <플래그에 연결된 대시보드나 알림이 있습니까?> 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": "새 원페이지 체크아웃", "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 기반 검색", "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": "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 테스트: 연간 요금제 우선", "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": "다크 테마", "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": "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": "베타 고객용 CSV 내보내기", "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": "이전 PDF 렌더러", "environments": {"production": {"on": false, "rollout": 0, "updated": "2026-02-01"}}}, {"key": "recs-carousel", "type": "release", "owner": "team-growth", "created": "2026-03-03", "description": "제품 페이지의 추천 캐러셀", "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("카드 결제가 일시 중지되었습니다. 잠시 후 다시 시도해 주세요."); } const oneClick = await flags.isEnabled("new-checkout-flow", user); return oneClick ? oneClickButton() : classicButton(); } export async function invoiceLink(order: Order) { // TODO 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="홀리데이 세일: 전품목 20% 할인" />} <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-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 # 기능 플래그 정리 감사 오늘 2026-11-05, 기본 환경 `production`, 30일 후 오래된 것으로 간주. 인벤토리에 9개의 플래그, examples/src에서 3개의 소스 파일 스캔. **판정: 높은 발견 3개 - 플래그 서비스가 모르는 코드에서 사용되는 플래그** 정리 후보: 6 (지금 제거 가능 4개, 변경할 참조 4개). 발견: 높음 3개, 중간 9개, 낮음 2개, 정보 3개. ## 정리 계획 | # | 플래그 | 유형 | 소유자 | 기본 | 작업 | 이유 | 참조 (파일) | 노력 | 위치 | |---|---|---|---|---|---|---|---|---|---| | 1 | `legacy-pdf-invoices` | release | team-billing | archived | **코드 제거** | 보관되었지만 여전히 코드에 있음 | 1 (1) | S | `examples/src/checkout.ts:20` | | 2 | `new-checkout-flow` | release | team-payments | 100% | **제거, 켜짐 유지** | 86일 동안 100% 켜짐 | 2 (1) | S | `examples/src/checkout.ts:4`, `examples/src/checkout.ts:14` | | 3 | `recs-carousel` | release | team-growth | 100% | **제거, 켜짐 유지** | 170일 동안 100% 켜짐 | 0 (0) | - | - | | 4 | `holiday-banner-2025` | release | team-growth | 0% | **제거, 꺼짐 유지** | 303일 동안 꺼짐 | 1 (1) | S | `examples/src/Header.tsx:5` | | 5 | `search-v2` | release | team-search | 25% | **롤아웃 완료** | 25%에서 멈춤 | 1 (1) | S | `examples/src/search.py:5` | | 6 | `pricing-page-test` | experiment | team-growth | 50% | **실험 결정** | 199일 동안 실행 중 | 1 (1) | S | `examples/src/Header.tsx:15` | 노력: S = 참조 1-2개, M = 3-6개, L = 7개 이상. "켜짐 유지" = 플래그 확인과 OFF 분기 삭제; "꺼짐 유지" = 보호된 코드 삭제. 코드 변경이 모든 곳에 배포된 후에만 플래그 서비스에서 플래그를 보관하십시오. ## 발견 | 심각도 | 코드 | 플래그 | 발견 | 수정 | |---|---|---|---|---| | 높음 | FF01 | `new-checkout-flow-v3` | 코드에서 사용됨 (examples/src/checkout.ts:27) 그러나 플래그 인벤토리에 정의되지 않음 - SDK가 기본값을 반환하므로 이 코드 경로는 잘못된 키를 읽고 있음 | new-checkout-flow의 오타? 정확한 키를 사용하십시오 | | 높음 | FF01 | `recs_carousel` | 코드에서 사용됨 (examples/src/search.py:20) 그러나 플래그 인벤토리에 정의되지 않음 - SDK가 기본값을 반환하므로 이 코드 경로는 잘못된 키를 읽고 있음 | recs-carousel의 오타? 정확한 키를 사용하십시오 | | 높음 | FF14 | `legacy-pdf-invoices` | 플래그가 플래그 서비스에서 보관되었지만 1곳에서 여전히 참조됨 - 이제 기본값을 반환함 | 참조 제거 | | 중간 | FF02 | `new-checkout-flow` | 릴리스 플래그 86일 동안 프로덕션에서 100% 켜짐 | 플래그를 제거하고 ON 경로 유지; OFF 분기와 이전 코드 삭제 | | 중간 | FF02 | `recs-carousel` | 릴리스 플래그 170일 동안 프로덕션에서 100% 켜짐 | 플래그를 제거하고 ON 경로 유지; OFF 분기와 이전 코드 삭제 | | 중간 | FF03 | `holiday-banner-2025` | 릴리스 플래그 303일 동안 프로덕션에서 꺼짐 - 보호된 코드는 죽은 코드임 | 플래그와 보호된 코드 제거 (또는 출시 결정) | | 중간 | FF05 | `holiday-banner-2025` | 2026-01-10에 만료됨 (299일 전) | 지금 제거하거나 소유자와 새 날짜 합의 | | 중간 | FF05 | `new-checkout-flow` | 2026-09-30에 만료됨 (36일 전) | 지금 제거하거나 소유자와 새 날짜 합의 | | 중간 | FF05 | `pricing-page-test` | 2026-07-01에 만료됨 (127일 전) | 지금 제거하거나 소유자와 새 날짜 합의 | | 중간 | FF08 | `dark-mode` | 프로덕션은 100%이지만 스테이징은 0% - 프로덕션은 스테이징이 테스트하지 않는 경로를 실행함 | 스테이징을 프로덕션과 정렬 | | 중간 | FF09 | `pricing-page-test` | 실험이 199일 동안 실행 중 (한도 90) | 결과를 발표하고, 승자를 출시하고, 플래그를 제거 | | 중간 | FF12 | `search-v2` | 롤아웃이 63일 동안 프로덕션에서 25%로 멈춤 - 사용자는 두 가지 버전을 얻음 | 롤아웃 완료 (100%) 또는 0으로 롤백한 다음 제거 | | 낮음 | FF04 | `recs-carousel` | 스캔한 3개 파일에 참조 없음 | 다른 저장소/서비스 확인; 사용되지 않으면 플래그 서비스에서 보관 | | 낮음 | FF07 | `beta-reports-export` | 소유자 없음 | 소유자 팀 또는 담당자 지정 | | 정보 | FF06 | `beta-reports-export` | 권한 플래그 - 설계상 장수명, 오래된 것으로 확인하지 않음 | 매년 검토; 설명과 소유자 유지 | | 정보 | FF06 | `payments-kill-switch` | 킬 스위치 플래그 - 설계상 장수명, 오래된 것으로 확인하지 않음 | 매년 검토; 설명과 소유자 유지 | | 정보 | FF10 | `-` | examples/src/search.py:14에서 런타임에 생성된 플래그 키 - 이에 대한 참조는 검색으로 찾을 수 없음 | 리터럴 키 (또는 상수 파일)를 선호하여 플래그를 정리할 수 있게 함 | ## 플래그별 참조 | 플래그 | 참조 | |---|---| | `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 | 검색은 스캔한 폴더의 문자열 리터럴, 일반적인 SDK 호출 및 `flags.camelCase` 속성만 다룹니다; 플래그를 보관하기 전에 다른 서비스, 모바일 앱 및 구성 저장소를 확인하십시오. FILE:scripts/flag_audit.py #!/usr/bin/env python3 """기능 플래그 오래된 항목 정리 감사자 (표준 라이브러리만, Python 3.8+). 기능 플래그 인벤토리 (LaunchDarkly, Unleash, Flagsmith, GrowthBook, ConfigCat 또는 사내 구성에서 내보내거나 수동으로 작성한 JSON)를 읽고 소스 트리에서 플래그 참조를 스캔합니다. 몇 주 동안 완전히 켜져 (또는 꺼져) 있어 제거할 수 있는 릴리스 플래그, 멈춘 부분 롤아웃, 만료 및 고아 플래그, 끝나지 않은 실험, 코드에서 사용되지만 플래그 서비스에 없는 (조용히 기본값으로 대체되는) 플래그, 환경 차이 및 소유자가 없는 플래그를 찾아 변경할 정확한 file:line 참조가 포함된 우선순위 정리 계획을 작성합니다. 사용법: flag_audit.py FLAGS.json SRC_DIR [SRC_DIR ...] [--today YYYY-MM-DD] [--stale-days N] [--call NAME ...] [--json] flag_audit.py --self-test 종료: 0 = 높은 발견 없음, 1 = 높은 발견 이상, 2 = 잘못된 입력. """ 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은(는) YYYY-MM-DD 형식이어야 합니다, 받은 값: %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\" 목록이 있는 JSON 객체여야 합니다") bad = set(cfg) - TOP_KEYS if bad: raise InputError("알 수 없는 최상위 키: %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("플래그 #%d에 키가 없습니다" % i) badk = set(f) - FLAG_KEYS if badk: raise InputError("플래그 %s: 알 수 없는 키 %s" % (f["key"], ", ".join(sorted(badk)))) t = (f.get("type") or "release").lower() if t not in TYPES: raise InputError("플래그 %s: 유형은 %s 중 하나여야 합니다, 받은 값: %r" % (f["key"], ", ".join(sorted(TYPES)), t)) envs = {} for e, st in (f.get("environments") or {}).items(): if not isinstance(st, dict): raise InputError("플래그 %s 환경 %s은(는) 객체 {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("플래그 %s 환경 %s: rollout은 0-100이어야 합니다, 받은 값: %r" % (f["key"], e, r)) envs[e] = {"on": bool(st.get("on")), "rollout": float(r), "updated": pdate(st["updated"], "플래그 %s 환경 %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"], "플래그 %s created" % f["key"]) if f.get("created") else None, "expires": pdate(f["expires"], "플래그 %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): """플래그를 받는 사용자의 유효 비율: 0-100""" return st["rollout"] if st["on"] else 0.0 # ---------------------------------------------------------------- scan def scan(dirs, keys, calls): """refs {key: [(path, line, text)]}, unknown {literal: [(path,line)]}, dynamic [(path,line,text)], files 반환""" 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("소스 디렉토리를 찾을 수 없음: %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("%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, "인벤토리에 플래그가 두 번 정의됨 (마지막 것이 사용됨)", "두 항목 병합") 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, "코드에서 사용됨 (%s) 그러나 플래그 인벤토리에 정의되지 않음 - SDK가 기본값을 반환하므로 이 코드 경로는 %s" % (locs, "아마 결코 켜지지 않음" if not close else "잘못된 키를 읽고 있음"), ("%s의 오타? 정확한 키를 사용하십시오" % close) if close else "플래그를 생성하거나 확인을 제거하십시오") for p, i, txt in dynamic[:20]: add("info", "FF10", "-", "%s:%d에서 런타임에 생성된 플래그 키 - 이에 대한 참조는 검색으로 찾을 수 없음" % (p, i), "플래그를 정리할 수 있도록 리터럴 키 (또는 상수 파일)를 선호하십시오") 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, "소유자 없음", "소유자 팀 또는 담당자 지정") if f["archived"]: if r: add("high", "FF14", k, "플래그가 플래그 서비스에서 보관되었지만 %d곳에서 여전히 참조됨 - 이제 기본값을 반환함" % len(r), "참조 제거") plan.append({"key": k, "type": f["type"], "owner": f["owner"] or "?", "state": "archived", "action": "remove code", "why": "보관되었지만 여전히 코드에 있음", "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, "기본 환경 %r에 대한 상태 없음" % prim, "%s 상태를 추가하거나 primary_env를 수정하십시오" % prim) if f["permanent"]: add("info", "FF06", k, "%s 플래그 - 설계상 장수명, 오래된 것으로 확인하지 않음" % f["type"], "매년 검토; 설명과 소유자 유지") if not r: add("low", "FF04", k, "스캔한 %d개 파일에 참조 없음" % nfiles, "다른 저장소/서비스 확인; 사용되지 않으면 플래그 서비스에서 보관") action, why = action or "archive", "참조 없음" 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, "%s에 만료됨 (%d일 전)" % (f["expires"], (today - f["expires"]).days), "지금 제거하거나 소유자와 새 날짜 합의") if f["type"] == "release" and e == 100 and days is not None and days >= stale: add("medium", "FF02", k, "릴리스 플래그 %d일 동안 %s에서 100%% 켜짐%s" % (days, prim, "" if all_on else " (모든 환경에서 100% 아님)"), "플래그를 제거하고 ON 경로 유지; OFF 분기와 이전 코드 삭제") action, why = "remove, keep ON", "%d일 동안 100%% 켜짐" % days elif e == 0 and days is not None and days >= stale and f["type"] in ("release", "experiment"): add("medium", "FF03", k, "%s 플래그 %d일 동안 %s에서 꺼짐 - 보호된 코드는 죽은 코드임" % (f["type"], days, prim), "플래그와 보호된 코드 제거 (또는 출시 결정)") action, why = "remove, keep OFF", "%d일 동안 꺼짐" % days elif 0 < e < 100 and days is not None and days >= stale and f["type"] == "release": add("medium", "FF12", k, "롤아웃이 %d일 동안 %s에서 %g%%로 멈춤 - 사용자는 두 가지 버전을 얻음" % (days, prim, e), "롤아웃 완료 (100%) 또는 0으로 롤백한 다음 제거") action, why = "finish rollout", "%g%%로 멈춤" % e if f["type"] == "experiment" and age is not None and age > c["exp_max"]: add("medium", "FF09", k, "실험이 %d일 동안 실행 중 (한도 %d)" % (age, c["exp_max"]), "결과를 발표하고, 승자를 출시하고, 플래그를 제거") action, why = action or "decide experiment", "%d일 동안 실행 중" % age if not r: add("low", "FF04", k, "스캔한 %d개 파일에 참조 없음" % nfiles, "다른 저장소/서비스 확인; 사용되지 않으면 플래그 서비스에서 보관") action, why = action or "archive", why or "참조 없음" 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은(는) %g%%이지만 %s은(는) %g%% - 프로덕션은 스테이징이 테스트하지 않는 경로를 실행함" % (prim, e, en, eff(s2)), "%s을(를) %s과(와) 정렬" % (en, prim)) if f["expires"] is None and f["type"] in ("release", "experiment") and action is None: add("low", "FF11", k, "만료 날짜가 없는 임시 %s 플래그" % f["type"], "expires 설정 (예: 전체 출시 후 30-60일)") 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 = {"remove code": 0, "remove, keep ON": 1, "remove, keep OFF": 2, "finish rollout": 3, "decide experiment": 4, "archive": 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("remove")] out = ["# 기능 플래그 정리 감사", "", "오늘 %s, 기본 환경 `%s`, %d일 후 오래된 것으로 간주. 인벤토리에 %d개의 플래그, %s에서 %d개의 소스 파일 스캔." % ( r["today"], r["primary"], r["stale_days"], r["flags"], r["files"], ", ".join(src)), ""] if c["high"]: v = "높은 발견 %d개 - 플래그 서비스가 모르는 코드에서 사용되는 플래그" % c["high"] elif removable: v = "제거할 준비가 된 플래그 %d개" % len(removable) else: v = "오래된 플래그 없음" out += ["**판정: %s**" % v, "", "정리 후보: %d (지금 제거 가능 %d개, 변경할 참조 %d개). 발견: 높음 %d개, 중간 %d개, 낮음 %d개, 정보 %d개." % ( len(r["plan"]), len(removable), sum(p["refs"] for p in removable), c["high"], c["medium"], c["low"], c["info"]), ""] out += ["## 정리 계획", "", "| # | 플래그 | 유형 | 소유자 | 기본 | 작업 | 이유 | 참조 (파일) | 노력 | 위치 |", "|---|---|---|---|---|---|---|---|---|---|"] for i, p in enumerate(r["plan"], 1): locs = ", ".join("`%s`" % x for x in p["locs"][:4]) + (" +%d개 더" % (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 += ["", "노력: S = 참조 1-2개, M = 3-6개, L = 7개 이상. \"켜짐 유지\" = 플래그 확인과 OFF 분기 삭제; \"꺼짐 유지\" = 보호된 코드 삭제. 코드 변경이 모든 곳에 배포된 후에만 플래그 서비스에서 플래그를 보관하십시오.", ""] out += ["## 발견", "", "| 심각도 | 코드 | 플래그 | 발견 | 수정 |", "|---|---|---|---|---|"] for s, code, k, msg, fix in r["findings"]: out.append("| %s | %s | `%s` | %s | %s |" % (s, code, k, msg.replace("|", "\\|"), fix.replace("|", "\\|"))) out += ["", "## 플래그별 참조", "", "| 플래그 | 참조 |", "|---|---|"] for k, n in sorted(r["refs"].items()): out.append("| `%s` | %d |" % (k, n)) out += ["", "검색은 스캔한 폴더의 문자열 리터럴, 일반적인 SDK 호출 및 `flags.camelCase` 속성만 다룹니다; 플래그를 보관하기 전에 다른 서비스, 모바일 앱 및 구성 저장소를 확인하십시오."] 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("%s을(를) 읽을 수 없음: %s" % (flags_path, e)) c = load_flags(cfg, today, stale) refs, unknown, dynamic, n = scan(dirs, list(c["flags"]), list(c["calls"]) + list(calls)) return audit(c, refs, unknown, dynamic, n) # ---------------------------------------------------------------- self-test def self_test(): import tempfile tests = [] def t(name, cond): tests.append((name, bool(cond))) def mk(flags, files, **kw): d = tempfile.mkdtemp() for name, body in files.items(): os.makedirs(os.path.dirname(os.path.join(d, name)) or d, exist_ok=True) open(os.path.join(d, name), "w").write(body) cfg = {"today": "2026-11-05", "flags": flags} cfg.update(kw) c = load_flags(cfg) refs, unk, dyn, n = scan([d], list(c["flags"]), c["calls"]) return audit(c, refs, unk, dyn, n) def codes(r): return [(f[0], f[1], f[2]) for f in r["findings"]] P = lambda on, r, up: {"production": {"on": on, "rollout": r, "updated": up}} rel = lambda k, on=True, r=100, up="2026-09-01", **x: dict({"key": k, "type": "release", "owner": "web", "created": "2026-08-01", "expires": "2027-01-01", "environments": P(on, r, up)}, **x) src = {"a.ts": "if (client.isEnabled('new-checkout')) {}\nconst x = useFlag(\"dark-mode\")\n"} r = mk([rel("new-checkout"), rel("dark-mode", up="2026-10-30")], src) t("65일 동안 100% 켜짐 -> FF02", ("medium", "FF02", "new-checkout") in codes(r)) t("6일 동안 100% 켜짐 -> 오래되지 않음", ("medium", "FF02", "dark-mode") not in codes(r)) t("계획 작업 켜짐 유지", [p["action"] for p in r["plan"] if p["key"] == "new-checkout"] == ["remove, keep ON"]) t("호출을 통한 참조 발견", 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("65일 동안 꺼짐 -> FF03 꺼짐 유지", ("medium", "FF03", "old-banner") in codes(r) and r["plan"][0]["action"] == "remove, keep OFF") r = mk([rel("half", r=50)], {"c.js": "isEnabled('half')"}) t("멈춘 롤아웃 -> FF12", ("medium", "FF12", "half") in codes(r)) r = mk([rel("new-checkout")], {"a.ts": "isEnabled('new_checkout')\n"}) t("코드의 정의되지 않은 키 -> FF01 높음", ("high", "FF01", "new_checkout") in codes(r)) t("FF01이 유사 키 제안", any("new-checkout의 오타" in f[4] for f in r["findings"])) t("FF01만으로 실제 키의 참조 0 -> FF04", ("low", "FF04", "new-checkout") in codes(r)) r = mk([rel("ghost")], {"a.ts": "const a = 1\n"}) t("참조 없음 -> FF04 + 보관", ("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("90일 이상 실험 -> FF09", ("medium", "FF09", "exp1") in codes(r)) r = mk([rel("exp2", expires="2026-10-01", up="2026-10-30")], {"a.ts": "isEnabled('exp2')"}) t("만료됨 -> FF05", ("medium", "FF05", "exp2") in codes(r)) r = mk([dict(rel("ks"), type="kill-switch", expires=None)], {"a.ts": "isEnabled('ks')"}) t("킬 스위치 건너뜀 -> FF06 정보만", [x[1] for x in codes(r)] == ["FF06"]) r = mk([dict(rel("p", up="2026-10-30"), owner="")], {"a.ts": "isEnabled('p')"}) t("소유자 없음 -> 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("프로덕션이 스테이징보다 앞섬 -> FF08", ("medium", "FF08", "d") in codes(r)) r = mk([rel("x")], {"a.ts": "const k = 'x' + region\nisEnabled(flagName)\n"}) t("동적 키 -> FF10 정보", ("info", "FF10", "-") in codes(r)) r = mk([rel("x")], {"a.py": "is_enabled('export-' + region)\n"}) t("리터럴 + 변수는 정의되지 않음이 아닌 동적", ("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 속성 계산", 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 및 비코드 파일 건너뜀", r["refs"]["new-search"] == 0) r = mk([rel("t", up="2026-10-30", expires=None)], {"a.ts": "isEnabled('t')"}) t("만료일 없음 -> FF11", ("low", "FF11", "t") in codes(r)) r = mk([rel("arch", archived=True)], {"a.ts": "isEnabled('arch')"}) t("보관되었지만 참조됨 -> FF14 높음", ("high", "FF14", "arch") in codes(r) and r["plan"][0]["action"] == "remove code") r = mk([rel("dup", up="2026-10-30"), rel("dup", up="2026-10-30")], {"a.ts": "isEnabled('dup')"}) t("중복 키 -> 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("사용자 지정 호출 이름이 정의되지 않은 키 발견", ("high", "FF01", "nope") in codes(r)) r = mk([rel("stale14", up="2026-10-20")], {"a.ts": "isEnabled('stale14')"}, stale_days=14) t("stale_days 설정 존중", ("medium", "FF02", "stale14") in codes(r)) for bad, name in (({"flags": [{"key": "a", "type": "temp"}]}, "잘못된 유형"), ({"flags": [{"key": "a", "environments": {"production": {"on": True, "rollout": 150}}}]}, "rollout > 100"), ({"flags": [{"key": "a", "colour": 1}]}, "알 수 없는 플래그 키"), ({"flag": []}, "flags 목록 누락")): try: load_flags(bad); t("거부됨: " + name, False) except InputError: t("거부됨: " + name, True) r = mk([rel("new-checkout")], src) t("보고서에 판정 및 계획 있음", "**판정:" in report(r, ["x"]) and "## 정리 계획" 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 통과" % (p, len(tests))) return 0 if p == len(tests) else 1 def main(argv=None): ap = argparse.ArgumentParser(description="오래된 기능 플래그를 찾고 제거를 계획합니다.") 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="추가 플래그 확인 함수 이름 (반복 가능)") 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("FLAGS.json과 최소한 하나의 소스 디렉토리를 지정하십시오") try: r = run(a.flags, a.src, a.today, a.stale_days, a.call) except InputError as e: print("error: %s" % e, file=sys.stderr) return 2 if a.json: print(json.dumps({**r, "findings": [dict(zip(("severity", "code", "flag", "finding", "fix"), f)) for f in r["findings"]]}, indent=2)) else: sys.stdout.write(report(r, a.src)) return 1 if any(f[0] == "high" for f in r["findings"]) else 0 if __name__ == "__main__": sys.exit(main())

전체 프롬프트를 보려면 로그인하세요

Continue with:

By logging in, you agree to our Terms of Use and Privacy Policy

사용법

이 프롬프트는 coding와 함께 사용하도록 설계되었습니다. 위의 프롬프트 내용을 복사하여 원하는 AI 도구에 붙여넣으세요.

최상의 결과를 얻으려면 자리 표시자(대괄호 또는 대문자로 표시)를 특정 요구 사항으로 사용자 지정할 수 있습니다.

참고 자료

분류:coding| prompts.chat| feature-flags| code-cleanup

토론