Add missing `feature_category` metadata to RSpec files clearing RSpec/FeatureCategory todos
<!--IssueSummary start-->
<details>
<summary>
Everyone can contribute. [Help move this issue forward](https://handbook.gitlab.com/handbook/marketing/developer-relations/contributor-success/community-contributors-workflows/#contributor-links) while earning points, leveling up and collecting rewards.
</summary>
- [Close this issue](https://contributors.gitlab.com/manage-issue?action=close&projectId=278964&issueIid=600670)
</details>
<!--IssueSummary end-->
## Summary
**3,295 spec files** are excluded from the `RSpec/FeatureCategory` cop via
[`.rubocop_todo/rspec/feature_category.yml`](../blob/master/.rubocop_todo/rspec/feature_category.yml)
due to a missing `feature_category:` tag on their top-level `RSpec.describe`.
To kick off, give an AI agent the following prompt:
> Work on https://gitlab.com/gitlab-org/gitlab/-/work_items/600670
Relates to: https://gitlab.com/groups/gitlab-org/quality/-/work_items/275
---
## Agent Guidelines
- **Batch size:** 25 files per MR (max 30).
- **Only add missing values.** Skip files that already have any `feature_category:` (even `:shared`); leave them in the rubocop_todo.
- **Valid categories:** `config/feature_categories.yml` + `tooling`, `test_platform`, `rails_platform`. `:shared` is **not** valid.
- **Top-level only.** Only modify the `RSpec.describe` line; never nested `context`/`describe`.
**Determining the category** (in order):
1. Source file — `feature_category :xxx` in the corresponding class
2. Sibling specs in the same directory that already have the tag
3. Base (non-EE) equivalent spec
4. CODEOWNERS group → team's primary category
5. Module/class name
---
## Learned patterns
### RuboCop cop specs (`spec/rubocop/`)
Match category to what the cop enforces, not just the directory:
| Cop type | Category |
|---|---|
| `cop/database/`, ActiveRecord/ORM performance cops | `:database` |
| `cop/api/`, `cop/graphql/` | `:api` |
| `cop/scalability/`, Sidekiq worker cops | `:scalability` |
| `cop/migration/` | `:database` |
| `cop/gitlab/feature_flag*`, `cop/feature_flag*` | `:scalability` |
| `cop/gettext/` | `:internationalization` |
| Security/file-safety cops | `:vulnerability_management` |
| General code quality / tooling helpers | `:tooling` |
| `cop/qa/`, QA helpers | `:test_platform` |
| `cop/usage_data/` | `:service_ping` |
| `cop/user_admin` | `:system_access` |
| `cop/static_translation_definition` | `:internationalization` |
| Sidekiq/Redis cops (`sidekiq_*`, `redis_*`) | `:scalability` |
`rails_platform` is for Rails core framework gems only (`rails`, `zeitwerk`) — not for cops enforcing Rails patterns.
**Important:** Many `spec/rubocop/cop/` files already have `feature_category: :shared` (set by the shared copconfig). Always check `grep "^RSpec.describe"` before editing — skip files that already have any `feature_category:` value.
### `spec/lib/gitlab/database/`
All files in this directory tree → `:database`.
### Running RuboCop locally
`bundle exec rubocop` may fail with `Could not find gitlab_query_language` (native gem missing in GDK setup). Use the system rubocop directly instead:
```shell
REVEAL_RUBOCOP_TODO=0 rubocop --only RSpec/FeatureCategory <files>
```
For commits/pushes, exclude the failing hooks:
```shell
LEFTHOOK_EXCLUDE=rubocop git commit ...
LEFTHOOK_EXCLUDE=openapi_docs,rubocop,danger,commit-message-linting git push origin <branch>
```
---
## Instructions
### Setup (once)
Resolve and reuse across all batches:
- **Current user ID:** `get_user` (no args) → `id`
- **Current milestone ID:** search `gitlab-org` group milestones for the lowest-version active milestone (e.g. `19.1`) → `id`
### Pre-flight (REQUIRED before every batch)
**Step 0 — Pull master.**
```shell
git checkout master && git pull origin master
```
**Step 1 — Build an exclusion set of already-claimed files.**
Use the GitLab MCP tool `search` (scope: `merge_requests`, project: `gitlab-org/gitlab`) to find all open MRs whose title contains `"Add feature_category metadata"`. For each result, call `list_merge_request_diffs` and collect every `new_path` ending in `_spec.rb`. Also call `list_merge_requests_related_to_issue` (issue IID 600670) and repeat for any open ones. Union all paths into a single exclusion set.
**Step 2 — Check for branch name collisions.**
```shell
git ls-remote --heads origin '*add-feature-category-batch-*'
```
The naming convention is `add-feature-category-batch-NNN`. Pick the next unused number (e.g. if `add-feature-category-batch-001` exists, use `add-feature-category-batch-002`).
**Step 3 — Select files (spread across the YAML to avoid conflicts).**
Read `.rubocop_todo/rspec/feature_category.yml`. Extract **all** entries (including those already excluded or tagged) to get the full list in YAML order. Call this list `all_entries` (length `N_total`).
**Conflict-avoidance — this takes priority over everything else.** The sole source of merge conflicts is `.rubocop_todo/rspec/feature_category.yml`. When multiple MRs are open simultaneously, their diff hunks must not overlap in that file. The critical insight: stride must be computed over **YAML positions** (the full entry list), not over the filtered candidate list. Striding over candidates fails when many consecutive YAML entries are all eligible — they map to adjacent YAML lines regardless of candidate-list distance.
1. Let `N_total` = total entries in the YAML file (all entries, not just candidates), `B` = batch size (25).
2. Compute stride `S = N_total / B` (round to nearest integer). With ~3100 entries this gives `S ≈ 125`.
3. For `i` in `0..B-1`: start at YAML index `i * S`. Advance forward until finding an entry that is (a) not in the exclusion set and (b) does not already have `feature_category:` on its `RSpec.describe` line. Select that entry. This guarantees each selected deletion is ~`S` YAML lines from the next — well beyond git's 3-line context window (which needs only 7 lines of separation to avoid conflicts).
**Secondary goal — minimize distinct CODEOWNERS groups.** After the stride selection, check whether the chosen files cluster into one or two CODEOWNERS groups. If swapping a few stride-selected files for nearby unclaimed ones (within ±S/4 positions **in the YAML**) would reduce the reviewer count without closing the gap between hunks below 20 lines, make that swap. Do not override the spread to achieve grouping — a few extra reviewers is cheaper than a conflict rebase.
### Per batch
1. **Edit:** add `, feature_category: :symbol` before `do` on the `RSpec.describe` line.
2. **Remove** the file from `.rubocop_todo/rspec/feature_category.yml`.
3. **Verify:**
```shell
REVEAL_RUBOCOP_TODO=0 rubocop --only RSpec/FeatureCategory <files>
```
4. **Commit:**
```
Add feature_category metadata to batch-NNN specs
Adds missing `feature_category:` metadata to N spec files and removes
them from .rubocop_todo/rspec/feature_category.yml.
Contributes to https://gitlab.com/gitlab-org/gitlab/-/issues/600670
```
Use the full URL — the commit-msg linter rejects short references.
5. **Push:**
```shell
LEFTHOOK_EXCLUDE=openapi_docs,rubocop,danger,commit-message-linting git push origin <branch>
```
(`openapi_docs` requires a dev database absent in standard GDK setups; `rubocop`/`danger`/`commit-message-linting` may fail if `gitlab_query_language` native gem is not installed.)
6. **Create MR** via GitLab MCP:
- `title`: `Add feature_category metadata to batch-NNN specs`
- `labels`: `pipeline::tier-1`
- `squash`: `true`, `remove_source_branch`: `true`
- `assignee_ids`: current user ID, `milestone_id`: current milestone ID
- `description`:
```markdown
## Summary
Adds missing `feature_category:` metadata to N spec files and removes
them from `.rubocop_todo/rspec/feature_category.yml`.
| File | `feature_category` | Rationale |
|---|---|---|
| `path/to/spec.rb` | `:category` | rationale |
Contributes to https://gitlab.com/gitlab-org/gitlab/-/issues/600670
```
7. **Update this issue** if new knowledge was gained during the batch — e.g. category assignment patterns, edge cases, or gotchas discovered — by editing the Agent Guidelines or adding to the Learned patterns section. This keeps future batches consistent.
---
## Definition of Done
- [ ] All files missing `feature_category:` have a valid tag
- [ ] `.rubocop_todo/rspec/feature_category.yml` contains only pre-existing non-standard values or is empty
- [ ] CI passes
- [ ] Every assigned category exists in `config/feature_categories.yml`
issue