- name
- bulk-sweep-classify
- description
- Classify every regex match before a bulk find-replace/syntax-modernization sweep — four match categories, scoped transform, allowlist-aware verification. Use when bulk-renaming commands/tools/paths or migrating syntax across many files.
- allowed-tools
- Bash, Read, Grep, Edit
- created
- 2026-07-05T00:00:00.000Z
- modified
- 2026-08-22T00:00:00.000Z
- reviewed
- 2026-07-06T00:00:00.000Z
# Bulk Sweep — Classify Every Match First
When you modernize a syntax across many files with a `regex` / `sed` / `perl`
pass — a command rename (`/ns:cmd` → `/ns-cmd`), a tool/binary rename, a path or
import-style migration, an API-version bump in prose — the matched set is
**heterogeneous even though every hit looks textually identical**. One pattern
catches genuine stale targets *and* several look-alikes that must NOT be
transformed. Blindly rewriting all matches corrupts the look-alikes silently: the
sweep reports success, and the damage surfaces far downstream (a broken designed
filename, a rewritten immutable record).
**The discipline: enumerate the matches and bucket every one into a category
before editing.** The regex sees *text*; the transform needs *semantics*.
## When to Use This Skill
| Use this skill when... | Use something else instead when... |
|------------------------|------------------------------------|
| A find-replace where the delimiter/token also appears in filenames, config keys, URLs, designs, or historical records | The pattern is structural (function shape, call form) → `code-quality-plugin:ast-grep-search`. **Also** when the rename is *textually simple but code-targeted* (an API rename, call-site migration, or import-path change in **source files**): route it through `ast-grep-search` first — structural matching won't touch strings/comments/URLs, so category-2 false positives shrink to near-zero (see the routing step below) |
| Command-syntax migration (`/ns:cmd` → `/ns-cmd`), tool rename in docs, path/import migration, API-version bump in prose | A one-off literal string search with no look-alike risk → `tools-plugin:rg-code-search` |
| Docs trees mixing live guidance with ADRs / changelogs / design PRDs, all matching the same regex | The whole match-set is genuinely uniform and you have verified it |
## The Four Categories
Read the surrounding line of each match and bucket it. Each category needs
different handling.
| Category | Handling | Canonical example (command-syntax sweep) |
|---|---|---|
| **1. Genuine stale target** | **Transform** | `/configure:mcp` → `/configure-mcp` in live docs |
| **2. False positive** — merely matches the pattern | **Leave** | `laurigates/dotfiles:latest` (docker tag); `redis://host:6379` (digit after `:`, not a command) |
| **3. Out-of-scope design** legitimately using the old form — esp. **delimiter embedded in designed filenames/paths** | **Leave** — transforming corrupts the design | `/sync:daily` PRD whose colon also appears in `sync:daily-state.json`, `.claude/commands/sync:daily.md` |
| **4. Immutable / historical record** matching the pattern | **Supersede-note; do NOT rewrite the body** | Accepted ADRs documenting the old `/namespace:command` convention |
**Category 3 is the sharpest trap.** The delimiter you're replacing (`:`, `/`,
`.`, `-`) often also appears in **filenames, config keys, or URLs** that the
matched token participates in. Hyphenating `/sync:daily` → `/sync-daily` also
rewrites every `sync:daily-state.json` path in the same doc — silently breaking a
design.
**Category 4** follows the standard supersede-don't-rewrite convention: set
`Status: Superseded` plus a top-note, leave the body as a historical record.
## Execution
### Step 0: Route by sweep target — code vs. prose/docs
Before enumerating, decide **what** you are sweeping. This picks the transform
engine and shrinks the classify workload:
- **Sweep target is code** — an API rename, call-site migration, or import-path
change in **source files**. Do the transform *structurally* with
`ast-grep -p '<old>' -r '<new>' --lang <l>`, delegating the transform
mechanics to `code-quality-plugin:ast-grep-search`. An ast-grep pattern
matches **AST nodes**, so it inherently won't match inside strings, comments,
or URLs — the whole **category-2 false-positive bucket shrinks to near-zero**.
The classify pass then focuses on **categories 3 (designed filenames/paths)
and 4 (immutable records) only**, and Steps 1–2's false-positive tightening is
largely unnecessary. Proceed to Step 3 with the ast-grep result in hand.
- **Sweep target is prose/docs/mixed** — command renames in markdown, tool names
in docs trees, an API-version bump in prose. Regex sees *text*, not semantics,
so the **four-category discipline below is unchanged and remains this skill's
core case.** Run the full Step 1 → Step 5 pipeline.
The decision hinges on whether a structural matcher *can* see your target: code
has an AST, prose does not. When in doubt (a rename that spans both source and
its surrounding docs), split it — ast-grep the source, then run the
four-category pass over the docs.
Run this classify-then-transform sweep:
### Step 1: Enumerate every match, deduped, before touching anything
```
git grep -nhoE '/[a-z][a-z0-9-]*:[a-z][a-z0-9-]*' -- <scope> | sort -u
```
Adjust the pattern to your migration. The point is a complete, deduped inventory
in hand *before* any edit.
#### Derive the term set from what the mechanism *claims*, not just what it is *named*
The enumeration is only as good as the terms you feed it. Removing a mechanism
means removing its **assertions** as well as its **identifiers**, and those two
families share no tokens — so a sweep built only from identifiers reports clean
while the claim survives.
After listing the identifier terms, ask:
**what did this mechanism promise, and in whose words?**
Add those phrasings to the term set. For a removal, terms usually come in two
families:
| Family | Example terms | Where they live |
|---|---|---|
| Identifiers | function, file, script, env var, config key | code, config, workflows |
| Claims | "baked in", "injected at build time", "automatically", "the published package …" | comments, README, docstrings, error and warning strings |
**The claim family is where user-facing damage concentrates.** An identifier
left behind is dead code, but
a claim left behind is documentation that is now false.
The incident behind this step (`podio-mcp` PRs #160 / #163, issue #2479 — a claim that
survived an identifier-only sweep) is in [references/evidence.md](references/evidence.md);
open it when you need a concrete example of a surviving claim.
### Step 2: Tighten the pattern to drop false positives at the source
Drive the false-positive exclusion into the **pattern** wherever you can, so the
sweep mechanically cannot touch category-2 hits. Example: requiring a letter
after the colon (`:[a-z]`) excludes `:6379` ports and `redis://` URLs. This
removes the whole class from manual consideration.
### Step 3: Bucket every remaining hit into categories 1–4
Read the surrounding line of each match. Reserve manual judgment for categories 3
and 4 — they *look* like real targets and can only be told apart by reading
intent. Record which files/lines fall into categories 2–4 (the
**intentionally-preserved set**).
### Step 4: Scope the transform to category-1 files ONLY
```
perl -i -pe 's{/([a-z][a-z0-9-]*):([a-z][a-z0-9-]*)}{/$1-$2}g' <category-1 files>
```
Pass only the category-1 files. Hand-handle categories 3 and 4:
category 3 is left untouched; category 4 gets a supersede/status note with its
body left intact.
### Step 5: Verify against the preserved set, not literal zero
Re-run the enumeration from Step 1. The correct success test is **NOT** "zero
matches remain":
> **Zero matches remain *outside the intentionally-preserved set*.**
Categories 3 and 4 are *supposed* to keep matching. Confirm the grep returns
**only** the category 2–4 lines you identified in Step 3.
For the **code route** (Step 0), the preserved set is whatever ast-grep's
structural match legitimately leaves behind — the old form still cited inside
**strings, comments, or URLs** that the AST matcher never touched. Verify it the
same way: re-run the enumeration and confirm the only remaining matches are those
non-code occurrences (plus any categories 3/4), not genuine call sites.
## The Verification Trap
The naive success test — *"re-run the grep; it should return nothing"* — is
**wrong**, because categories 3 and 4 legitimately still match. A verification
that demands literal zero will either fail spuriously or — worse — pressure you
into corrupting a category-3 design or rewriting a category-4 record just to force
the count to zero. Enumerate the preserved buckets up front, then verify the grep
returns *only* those.
The adjacent trap sits one level up, in the scope the verification runs over:
**every exclusion in a verification scope is a claim about the world, and needs
the same treatment as a negative result.** "Generated, so it doesn't count"
assumes the generator runs — check that it does. Cheap test: for each excluded
path, state why in one line, then verify that line. The #2479 case where an
excluded `docs/api/` turned out to be the only rendered copy is in
[references/evidence.md](references/evidence.md).
## Agentic Optimizations
Command templates for each step (enumerate, count, preview, apply, verify) are in
[references/commands.md](references/commands.md).
## Related
Adjacent rules and skills (intent verification, ast-grep, parallel dispatch, adversarial review):
[references/related.md](references/related.md).
For parallelizing a sweep across agents (the pre-dispatch rename map and do-NOT-rename list) and for briefing adversarial auditors, see [REFERENCE.md](REFERENCE.md).
Ver en GitHub