From 7de4f3ab1f166b14b89b8dd98ea4bcc75297a947 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 8 Apr 2026 13:17:26 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .../skills/cleanup-feature-toggles/SKILL.md | 320 ++++++++++++++++++ .../utils/annotations/RemoveWithToggle.kt | 32 ++ 2 files changed, 352 insertions(+) create mode 100644 .claude/skills/cleanup-feature-toggles/SKILL.md create mode 100644 core/utils/src/main/java/com/tangem/utils/annotations/RemoveWithToggle.kt diff --git a/.claude/skills/cleanup-feature-toggles/SKILL.md b/.claude/skills/cleanup-feature-toggles/SKILL.md new file mode 100644 index 0000000000..61a7873d5e --- /dev/null +++ b/.claude/skills/cleanup-feature-toggles/SKILL.md @@ -0,0 +1,320 @@ +--- +name: cleanup-feature-toggles +description: Remove released feature toggles (version <= target) — deletes from config, removes toggle properties, inlines `true` in calling code, removes dead branches. CI-safe, no prompts. +allowed-tools: Read, Grep, Glob, Bash, Edit, Write, Agent +argument-hint: [--dry-run] [--only ] +--- + +Remove all feature toggles whose version is less than or equal to the target release version. + +**CRITICAL: This skill runs on CI. NEVER ask questions. If anything is ambiguous, make the safer choice or skip the toggle.** + +## Constants + +- **Config file**: `core/config-toggles/src/main/assets/configs/feature_toggles_config.json` +- **Generated enum** (DO NOT edit): `core/config-toggles/build/generated/source/toggles/com/tangem/core/configtoggle/FeatureToggles.kt` +- **Dry-run mode**: check if `$ARGUMENTS` contains `--dry-run`. In dry-run mode, make NO file changes — only output what WOULD be removed (including affected files and usage sites). +- **Version**: extract the version number from `$ARGUMENTS` (e.g., `5.35`, `5.35.0`). The version is the first argument that matches a semver-like pattern (`X.Y` or `X.Y.Z`). +- **Only mode**: check if `$ARGUMENTS` contains `--only `. If present, process ONLY the specified toggle (it must still satisfy the version check). Multiple `--only` flags can be provided. + +## Phase 0: Preflight Checks + +### 0a. Parse Arguments + +Extract ``, optional `--dry-run`, and optional `--only ` (repeatable) from `$ARGUMENTS`. + +- If no version found: STOP with `FATAL: No version provided. Usage: /cleanup-feature-toggles [--dry-run] [--only ]` +- Validate version matches pattern `\d+\.\d+(\.\d+)?` — if not, STOP with `FATAL: Invalid version format.` +- Normalize version: if only `X.Y` is given, treat as `X.Y.0` for comparison. +- If `--only` flags are present, collect the toggle names into a filter list. + +### 0b. Verify Git State + +```bash +git status --porcelain 2>&1 +``` +- If output is empty (clean working tree) — OK. +- If there are uncommitted changes — STOP with: `FATAL: Working tree is not clean. Commit or stash changes before running this skill.` + +Initialize an internal results list to track each toggle's outcome. + +## Phase 1: Identify Toggles to Remove + +1. Read `core/config-toggles/src/main/assets/configs/feature_toggles_config.json`. +2. For each toggle entry in the JSON array: + - If `version == "undefined"` → skip (unreleased feature, must not be removed). + - Parse the toggle's version as semver (normalize `X.Y` to `X.Y.0`). + - If toggle version **<=** target version → mark for removal. +3. If `--only` filter is active: keep only toggles whose `name` matches one of the `--only` values. If a `--only` toggle doesn't satisfy the version check, output a warning but still skip it. +4. Output the list of toggles marked for removal with their versions. +5. If no toggles match → output `No toggles to remove for version ` and stop. +6. If `--dry-run` mode → proceed to Phase 2 (research only, all toggles in parallel), then skip to Phase 7 to output the detailed summary. Do NOT make any file changes. + +## Phase 2: Research (parallel) + +Collect all information about all toggles **in parallel** before making any edits. Launch one `Agent` per toggle (all in a single message so they run concurrently). Each agent receives the toggle name and must return a structured report. + +**Error handling rule**: if research fails for a toggle, record the failure reason and continue. Do NOT stop processing. + +### Per-toggle research task (runs inside each Agent) + +Each Agent performs the following read-only searches and returns a structured report: + +#### 2a. Find the toggle property declaration and direct usages + +Use `Grep` to search for `FeatureToggles.` (e.g., `FeatureToggles.WALLET_REORDER_FEATURE_ENABLED`) across the **entire** codebase. + +This will find: +1. **`DefaultXxxFeatureToggles` property** — the standard wrapper. Extract: + - The **property name** (e.g., `isWalletReorderFeatureEnabled`) + - The **DefaultXxxFeatureToggles file path** + - The **XxxFeatureToggles interface name** (from the class's supertype) +2. **Direct `FeatureTogglesManager.isFeatureEnabled()` calls** — code that bypasses the wrapper and calls the manager directly. These are additional usage sites. + +Also find the interface file: +- Use `Glob` to find the `XxxFeatureToggles.kt` file in `features/*/api/` or `core/*/` + +Check if the toggle property or the `DefaultXxxFeatureToggles` class has **comments referencing additional cleanup** (e.g., `// Remove GiveTxPermissionBottomSheet and all dependencies with this toggle`). If found, record the comment text. + +If the toggle reference is not found anywhere: report as `Skipped (no property found)`. + +#### 2b. Find `@RemoveWithToggle` annotated code + +Use `Grep` to search for `RemoveWithToggle` (without `@` or package prefix) across the entire codebase (excluding the annotation definition itself). Then filter matches to only those where the `toggleName` argument equals the current toggle name. + +The annotation is defined in `core/utils/src/main/java/com/tangem/utils/annotations/RemoveWithToggle.kt` (`com.tangem.utils.annotations.RemoveWithToggle`). It has two parameters: `toggleName: String` (the toggle name) and `description: String` (optional hint). + +Support all Kotlin annotation forms: +- `@RemoveWithToggle("TOGGLE_NAME")` +- `@RemoveWithToggle(toggleName = "TOGGLE_NAME")` +- `@com.tangem.utils.annotations.RemoveWithToggle("TOGGLE_NAME")` +- `@com.tangem.utils.annotations.RemoveWithToggle(toggleName = "TOGGLE_NAME")` + +For each filtered match, record: +- The file path and line number +- The annotated element name (class, function, property) +- The `description` value if present + +#### 2c. Find all usages of the property in calling code + +Use `Grep` to search for the property name (e.g., `isWalletReorderFeatureEnabled`) across the entire codebase. + +Categorize results: +- **Interface declaration** — the `val isX: Boolean` in `XxxFeatureToggles.kt` +- **Implementation** — the `override val isX` in `DefaultXxxFeatureToggles.kt` +- **Calling code** — any other file that reads `*.isX` (include file path, line number, and the matched line content) + +#### Agent report format + +Each Agent must return a report with: +- Toggle name +- Property name (e.g., `isWalletReorderFeatureEnabled`) or `null` if not found +- Interface name and file path +- Implementation file path +- List of calling code sites: `[{file, line, content}]` +- List of direct `FeatureTogglesManager` usage sites: `[{file, line, content}]` +- List of `@RemoveWithToggle` sites: `[{file, line, element, description}]` +- Cleanup comments (if any) +- Status: `ready` or `skipped (reason)` + +### After all Agents complete + +Collect all reports. If `--dry-run` → skip to Phase 6 with the collected data. + +## Phase 3: Edit (sequential) + +Process each toggle **sequentially** using the research data from Phase 2. Only toggles with status `ready` are processed. + +**Error handling rule**: if ANY step fails for a toggle, record the failure reason and continue to the next toggle. Do NOT stop processing. + +### Step 3a: Replace usages in calling code with `true` and simplify + +For each calling code usage site (from Phase 2 report), `Read` the surrounding context (at least 20 lines around the usage) and apply the appropriate simplification: + +| Pattern | Simplification | +|---------|---------------| +| `if (toggles.isX) { body }` | Remove `if`, keep `body` (unindent) | +| `if (toggles.isX) { A } else { B }` | Keep only `A`, remove if/else structure | +| `if (!toggles.isX) { body }` | Remove entire if-block | +| `if (!toggles.isX) { A } else { B }` | Keep only `B`, remove if/else structure | +| `toggles.isX && expr` | Replace with `expr` | +| `expr && toggles.isX` | Replace with `expr` | +| `toggles.isX \|\| expr` | Replace with `true` (or simplify enclosing condition since it's always true) | +| `val x = toggles.isX` | Replace with `val x = true`, then check if `x` is used in one of the patterns above and simplify transitively | +| `property = toggles.isX` | Replace with `property = true` | +| `when { toggles.isX -> A; else -> B }` | Keep only `A`, remove the `when` structure | +| `when { !toggles.isX -> A; else -> B }` | Keep only `B`, remove the `when` structure | +| `when(value) { ... }` with toggle in a branch condition | Evaluate the toggle to `true`, simplify the `when` accordingly | +| Complex boolean expression | Replace `toggles.isX` with `true` and algebraically simplify | + +Also process any direct `FeatureTogglesManager.isFeatureEnabled()` call sites the same way (replace with `true` and simplify). + +**After replacing**, check if the file still references the `XxxFeatureToggles` type: +- If not → remove the import of `XxxFeatureToggles` +- If the type was a constructor/inject parameter and is no longer used → remove the parameter and any `@Inject`/`@Assisted` annotations associated with it +- If removing a constructor parameter from a Decompose Model or Component, also remove it from the caller that creates the instance + +**Important**: Use `Edit` for precise changes. Read enough context to make correct edits. Do NOT accidentally delete unrelated code. + +### Step 3b: Remove the property from interface and implementation + +1. **In `XxxFeatureToggles` interface**: remove the `val isPropertyName: Boolean` line. +2. **In `DefaultXxxFeatureToggles`**: remove the `override val isPropertyName: Boolean` property (including the `get() = ...` line). +3. Check if `DefaultXxxFeatureToggles` still has other properties: + - If **yes** → done with this toggle. + - If **no** (all properties removed) → the interface and implementation are now empty. Check the **protected list** below — if the interface is protected, keep it and skip deletion. Otherwise, delete them: + +**Protected interfaces (never delete even if empty):** +- `TokensFeatureToggles` +- `BlockchainSDKFeatureToggles` +- `StakingFeatureToggles` +- `CardSdkFeatureToggles` +- `TangemPayFeatureToggles` +- `SwapFeatureToggles` +- `SendFeatureToggles` + +If the interface is **protected**: remove the `featureTogglesManager` / `featureToggles` constructor parameter from `DefaultXxxFeatureToggles`, remove unused imports (`FeatureTogglesManager`, `FeatureToggles`), but keep both files. + +If the interface is **not protected** → delete them: + 1. Delete the `XxxFeatureToggles` interface file. + 2. Delete the `DefaultXxxFeatureToggles` implementation file. + 3. Find and remove the Hilt binding for this interface (typically a `@Binds` method in a `*FeatureTogglesModule` or similar Hilt module). If the Hilt module has no remaining bindings after removal, delete the module file as well. + 4. Use `Grep` to find all remaining references to `XxxFeatureToggles` and `DefaultXxxFeatureToggles` across the codebase. For each reference: + - **Constructor/inject parameter** → remove the parameter. If the surrounding class/function no longer uses any feature toggles, cascade the removal to its callers. + - **Import statement** → remove it. + - **Any other reference** → assess and remove or update as needed. + +Record status as `Removed` with the count of usage sites simplified. + +## Phase 4: Update JSON Config + +1. Read `feature_toggles_config.json`. +2. Remove all entries whose `name` matches a successfully removed toggle (status = `Removed`). +3. Write back the JSON with proper formatting: + - 2-space indentation + - Each entry on its own lines + - No trailing commas + - Match the existing file format exactly + +## Phase 5: Build, Test & Lint Verification + +Run all verification tasks in a **single Gradle invocation** to avoid repeated cold starts: + +### 5a. Build + Tests + Detekt + +```bash +./gradlew assembleGoogleDebug unitTest detekt detektMain :app:assembleGoogleMocked :app:assembleGoogleMockedAndroidTest +``` + +- If the command **fails**: + - Read the error output to determine which task failed. + - **Compilation error** (`assembleGoogleDebug` or `assembleGoogleMocked`): attempt to fix (one retry — usually missing import removal or unused parameter). If still fails: revert all changes with `git checkout -- .` and output `FATAL: Build failed after cleanup. All changes reverted.` with the error details. + - **Unit test failure**: attempt to fix (one retry). If still fails: output the failures as warnings in the summary but do NOT revert. + - **Detekt violation**: attempt to fix (one retry — usually unused imports or parameters). If still fails: output the violations as warnings in the summary. + - **UI test compilation failure**: attempt to fix (one retry). If still fails: output the failures as warnings in the summary. + - After fixing, re-run the **full command** to verify everything passes together. + +## Phase 6: Branch, Commit, Push & PR + +Skip this phase entirely in `--dry-run` mode. + +### 6a. Create Branch and Commit + +```bash +git checkout -b tech/cleanup-toggles- +git add -A +git commit -m "[Tech] Remove feature toggles <= " +``` + +Replace `` with the target version (e.g., `tech/cleanup-toggles-5.35`). + +### 6b. Push + +```bash +git push -u origin tech/cleanup-toggles- +``` + +### 6c. Create Pull Request + +Use `gh pr create` targeting `develop`: + +```bash +gh pr create --base develop --title "Remove feature toggles <= " --body "$(cat <<'EOF' +## Summary + +Automated cleanup of feature toggles that are permanently enabled (version <= ). + +### Removed toggles + +- `TOGGLE_NAME_1` (version) +- `TOGGLE_NAME_2` (version) +- ... + +### Manual review required + + + +## Test plan + +- [x] `assembleGoogleDebug` passes +- [x] `unitTest` passes +- [x] `detekt detektMain` passes + +🤖 Generated with [Claude Code](https://claude.com/claude-code) +EOF +)" +``` + +Output the PR URL. + +## Phase 7: Output Summary + +Output results as a Markdown table: + +```markdown +## Feature Toggle Cleanup Summary + +| Toggle | Version | Interface | Usages Simplified | Status | +|--------|---------|-----------|-------------------|--------| +| WALLET_REORDER_FEATURE_ENABLED | 5.34 | WalletFeatureToggles | 3 | Removed | +| EARN_BLOCK_ENABLED | 5.35 | EarnFeatureToggles | 1 | Removed | +| SOME_TOGGLE | 5.33 | SomeFeatureToggles | — | Skipped (no property found) | + +**Total:** X toggles processed, Y removed, Z skipped/failed +**Target version:** +``` + +### Manual Review Hints + +If any toggle had a comment referencing additional cleanup (found in Phase 2 research), output a separate section: + +```markdown +### Manual Review Required + +- **GASLESS_APPROVAL_ENABLED**: `// Remove GiveTxPermissionBottomSheet and all dependencies with this toggle` +- **OTHER_TOGGLE**: `// Also remove legacy FooBar component` +``` + +### Dry-run mode output + +In `--dry-run` mode: prepend `[DRY RUN]` to the header, set all statuses to `Would remove`, and add a detailed section per toggle: + +```markdown +### WALLET_REORDER_FEATURE_ENABLED (5.34) — Would remove + +**Property:** `WalletFeatureToggles.isWalletReorderFeatureEnabled` +**Files affected:** +- `features/details/impl/.../UserWalletListModel.kt:42` — `walletFeatureToggles.isWalletReorderFeatureEnabled && userWallets.size > 1` +- `features/wallet/impl/.../SomeOtherFile.kt:88` — `if (walletFeatureToggles.isWalletReorderFeatureEnabled)` +``` + +### Warnings + +If unit tests or detekt failed after fix attempts, list the remaining issues: + +```markdown +### Warnings + +- **Unit test failure:** `:features:wallet:impl:testDebugUnitTest` — WalletModelTest.someTest (may need manual update) +- **Detekt violation:** UnusedPrivateMember in `SomeFile.kt:15` +``` \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/annotations/RemoveWithToggle.kt b/core/utils/src/main/java/com/tangem/utils/annotations/RemoveWithToggle.kt new file mode 100644 index 0000000000..4a9f4f9468 --- /dev/null +++ b/core/utils/src/main/java/com/tangem/utils/annotations/RemoveWithToggle.kt @@ -0,0 +1,32 @@ +package com.tangem.utils.annotations + +/** + * Marks code that should be removed when the specified feature toggle is cleaned up + * by the `/cleanup-feature-toggles` skill. + * + * @property toggleName the name of the feature toggle (e.g., "GASLESS_APPROVAL_ENABLED") + * @property description optional description of what should be done during cleanup + * +[REDACTED_AUTHOR] + */ +@Target( + AnnotationTarget.CLASS, + AnnotationTarget.ANNOTATION_CLASS, + AnnotationTarget.PROPERTY, + AnnotationTarget.FIELD, + AnnotationTarget.LOCAL_VARIABLE, + AnnotationTarget.VALUE_PARAMETER, + AnnotationTarget.CONSTRUCTOR, + AnnotationTarget.FUNCTION, + AnnotationTarget.PROPERTY_GETTER, + AnnotationTarget.PROPERTY_SETTER, + AnnotationTarget.TYPE, + AnnotationTarget.EXPRESSION, + AnnotationTarget.FILE, + AnnotationTarget.TYPEALIAS, +) +@Retention(AnnotationRetention.SOURCE) +annotation class RemoveWithToggle( + val toggleName: String, + val description: String = "", +) \ No newline at end of file