# Rule Cookbook

Seventeen recipes you can copy straight into a `NamingRuleSet`. Most are single rules; four —
numbers 4, 15, 16 and 17 — are project-wide settings on the rule set itself, and say so. Each recipe
gives the exact field values to type, what it catches, and what it deliberately does not.

Rules are edited in the **Inspector**, on the rule set asset itself. The window's Rules tab is a
read-only view: it shows you what each rule already demands and lets you switch one off, but it does
not add rules or edit fields. To get to the Inspector, click **Edit in Inspector** on the Rules tab
— it selects the rule set and pings it in the Project window — or just click the asset. Then expand
**Rules**, press **+** on the list, and fill in the new row. Anything not listed in a recipe should
stay at its default.

The field names below are the ones in the API and in the asset's YAML. The Inspector nicifies them,
so `pathFilters` shows as `Path Filters`, `allowAutoFix` as `Allow Auto Fix`, and so on. There is no
Save button; Unity saves the asset like any other.

---

## Before you copy anything

**Six things that will save you an hour.**

1. **Exactly one rule applies to each asset.** The most specific rule wins:
   `pathFilters` scores 8, `extensions` 4, `typeNames` 4, and the scores add up. A tie goes to
   whichever rule sits higher in the list. Several recipes below depend on this.
2. **`pathFilters` is not `allowedFolders`.** `pathFilters` decides *which assets this rule looks
   at*. `allowedFolders` decides *where those assets must live*. Setting both to the same glob
   means the folder check can never fire.
3. **Field formats differ.** `extensions`, `typeNames`, `requiredPrefix` and `requiredSuffix` are
   single text fields split on `,` `|` or `;`. `pathFilters`, `allowedFolders` and
   `forbiddenFolders` are string arrays — one glob per element.
4. **Only the name body is rewritten, never the file type.** The Cop rebuilds the name and
   reattaches the extension, but it reattaches it lower-cased: `PlayerIdle.PNG` comes back as
   `.png`. Matching is case-insensitive either way, so the rule sees the file whichever case it
   arrived in.
5. **Backslashes are literal in a rule field.** Write `\d{2}`, not `\\d{2}`.
6. **Casing values are enum names, not examples.** In the Inspector the `convention` dropdown reads
   `Snake Case` and `Pascal Snake Case`; in code and in YAML they are `SnakeCase` and
   `PascalSnakeCase`. The recipes below use the code names. The full set is `Any`, `PascalCase`,
   `CamelCase`, `SnakeCase`, `ScreamingSnakeCase`, `KebabCase` and `PascalSnakeCase`.

**Glob syntax:** `*` is any run of characters except `/`, `**` is any run including `/`, `?` is
one character except `/`. A pattern with no wildcard is a "contains this folder segment" test, so
`Art/Textures` matches `Assets/Game/Art/Textures/Hero/T_Hero.png`.

---

## 1. Unreal-style texture prefixes

Your team came from Unreal, or you want the Project window search box to be useful. Everything
starts with `T_`.

| Field | Value |
|---|---|
| `displayName` | `Textures` |
| `severity` | `Warning` |
| `extensions` | `.png, .jpg, .jpeg, .tga, .psd, .exr` |
| `pathFilters` | `Assets/_Game/**` |
| `requiredPrefix` | `T_, TEX_` |
| `convention` | `PascalCase` |
| `minLength` | `4` |
| `maxLength` | `64` |
| `allowAutoFix` | on |
| `note` | `T_ is the house standard. TEX_ is tolerated on art imported before 2025.` |

Several prefixes are allowed and **any one of them satisfies the rule**. A name that already
starts with `TEX_` keeps it; a name with no recognised prefix gets the first one in the list,
`T_`.

| Before | After |
|---|---|
| `player run.png` | `T_PlayerRun.png` |
| `hero_diffuse copy.png` | `T_HeroDiffuse.png` |
| `TEX_rock.tga` | `TEX_Rock.tga` |

PascalCase removes underscores, so `TEX_hero_01.png` becomes `TEX_Hero01.png`. If you want a
trailing `_01` to survive, use recipe 7 instead.

---

## 2. Materials and static meshes, Unreal style

The companions to recipe 1. Two more rules.

**Materials**

| Field | Value |
|---|---|
| `displayName` | `Materials` |
| `severity` | `Warning` |
| `extensions` | `.mat` |
| `pathFilters` | `Assets/_Game/**` |
| `requiredPrefix` | `M_, MI_` |
| `convention` | `PascalCase` |
| `note` | `M_ for a material, MI_ for a material instance made from it.` |

**Meshes**

| Field | Value |
|---|---|
| `displayName` | `Meshes` |
| `severity` | `Warning` |
| `extensions` | `.fbx, .obj, .blend` |
| `pathFilters` | `Assets/_Game/**` |
| `requiredPrefix` | `SM_, SK_` |
| `convention` | `PascalCase` |
| `note` | `SM_ for a static mesh, SK_ for a skinned one. Set the prefix before you import.` |

Match by extension, not by `typeNames`. A `typeNames` matcher forces
`AssetDatabase.GetMainAssetTypeAtPath` on every asset in the scan, which is the slowest thing the
scanner can do. Extensions cost nothing.

Renaming an FBX keeps its GUID, so materials, prefabs and animator references all follow. The
mesh names *inside* the FBX are authored in your DCC tool and are not touched.

---

## 3. UI sprites must live in one folder

Every sprite under any folder called `UI` belongs in one canonical sprites folder, prefixed `UI_`.

| Field | Value |
|---|---|
| `displayName` | `UI Sprites` |
| `severity` | `Warning` |
| `extensions` | `.png, .psd` |
| `pathFilters` | `**/UI/**` |
| `requiredPrefix` | `UI_` |
| `convention` | `PascalCase` |
| `allowedFolders` | `Assets/_Game/UI/Sprites/**` |
| `preferredFolder` | `Assets/_Game/UI/Sprites` |
| `allowAutoFix` | on |
| `note` | `All UI art lives in one folder so the atlas packer has one place to look.` |

Specificity 12 — a path filter plus extensions. The texture rule from recipe 1 scores 12 as well,
because it is scoped too, so the two are **tied** and the tie goes to whichever sits higher in the
`rules` list. Drag this one above Textures, or the general rule claims your UI sprites and the `UI_`
prefix never fires. A tie between rules is the single most common reason a recipe appears to do
nothing.

`preferredFolder` only does something when `allowedFolders` is set. With both filled in, an asset
in the wrong place gets a **Wrong folder** finding and the fix moves it. Missing destination
folders are created.

**What this will not catch:** a UI sprite that was dropped into `Assets/Art` and never went near
a folder called `UI`. `pathFilters` means the rule never looks at it. There is no matcher that
keys off the file name, so if strays are your real problem, widen `pathFilters` to the whole
project and accept that every `.png` is then expected under `UI/Sprites` — which is usually worse.
Fix the strays by hand with `Assets ▸ Asset Naming Manager ▸ Fix Selected`.

---

## 4. Ban spaces and symbols everywhere

This is not a rule. It is a project-wide check on the rule set itself, so no rule can override it
and it applies to folder names as well as file names.

On the `NamingRuleSet`, under **Characters**:

| Field | Value |
|---|---|
| `forbiddenCharacters` | `` !@#$%^&*()+=[]{};'",<>?`~ `` with a leading space |
| `asciiOnly` | on |
| `illegalCharacterSeverity` | `Error` |

The leading space is the important character. Spaces break command-line tooling, asset bundle
names and some version control clients. `asciiOnly` additionally flags accented and non-Latin
names, which are the classic cause of a build that works on one artist's machine and nowhere else.

Removed characters become `_`, then each run of separators collapses down to its **first** one and
any trailing separator is dropped — so `Player  Run -- 01 .png` becomes `Player_Run_01.png` before
the casing rules even run. Note the hyphens: they are not in the forbidden list, but they were
swallowed by the collapse because an underscore reached that run first.

Do not put `/` in the list. It is a path separator and can never appear in a file name anyway;
`NamingRuleSet.Validate()` will tell you so.

**To ban spaces in only one folder** instead of project-wide, use a rule with a banned pattern:

| Field | Value |
|---|---|
| `displayName` | `No spaces in shipped content` |
| `pathFilters` | `Assets/_Game/**` |
| `customRegex` | `\s` |
| `regexMustMatch` | **off** |
| `convention` | `Any` |

`regexMustMatch` off means "the name must **not** match this". Regex findings are reported but
never auto-fixed, so this reports only.

---

## 5. Scripts PascalCase, report only

C# requires the file name to match the class name for any `MonoBehaviour`. That makes an automatic
rename actively dangerous, so this rule reports and stops.

| Field | Value |
|---|---|
| `displayName` | `Scripts` |
| `severity` | `Warning` |
| `extensions` | `.cs` |
| `pathFilters` | `Assets/_Game/Scripts/**` |
| `convention` | `PascalCase` |
| `minLength` | `3` |
| `maxLength` | `64` |
| `allowAutoFix` | **off** |
| `note` | `Rename the class in your IDE first, then the file. Never bulk-fix scripts here.` |

`allowAutoFix` off means this rule's own checks never propose a rename. A finding still appears in
the report with the explanation and this note, but there is no before-and-after to apply.

`PascalCase` accepts an interface name such as `IDamageable` and a generic helper such as
`ObjectPool`. It rejects `player_controller` and `playerController`, which is the point.

Note that the project-wide checks — illegal characters, junk words, number padding — are not part
of any rule, so a script called `Player Controller copy.cs` can still get a suggested clean name
even with `allowAutoFix` off. Do not tick that row either.

---

## 6. ScriptableObject configs with an SO_ prefix

| Field | Value |
|---|---|
| `displayName` | `Config assets` |
| `severity` | `Warning` |
| `extensions` | `.asset` |
| `pathFilters` | `Assets/_Game/Configs/**` |
| `requiredPrefix` | `SO_` |
| `convention` | `PascalCase` |
| `allowedFolders` | `Assets/_Game/Configs/**` |
| `preferredFolder` | `Assets/_Game/Configs` |
| `note` | `Every designer-editable config lives here so the data is reviewable in one diff.` |

`.asset` is the extension Unity uses for a great many things — render pipeline assets, input
action assets, presets, sprite atlas settings. Without `pathFilters` this rule would demand `SO_`
on all of them.

**The precise alternative** is to match your own classes by name:

| Field | Value |
|---|---|
| `typeNames` | `WeaponConfig, EnemyConfig, LootTable` |
| `extensions` | (leave empty) |
| `pathFilters` | (leave empty) |

`typeNames` is compared against the main asset type's short class name, which for a
ScriptableObject is your own class. This catches a config wherever it has wandered to. It costs a
type lookup on every asset in the scan, so use it when correctness matters more than a second of
scan time.

---

## 7. Animation clips numbered `_01`

Two shapes, both fully auto-fixable. Pick on how you want the name to read, not on what the tool
can manage — the trailing `_01` survives either way, because every convention here accepts a
segment that starts with a digit.

### Variant A — SnakeCase, fully auto-fixable

On the `NamingRuleSet`, under **Numbering**:

| Field | Value |
|---|---|
| `checkNumberPadding` | on |
| `numberPaddingWidth` | `2` |
| `numberPaddingSeverity` | `Warning` |

The rule:

| Field | Value |
|---|---|
| `displayName` | `Animation clips` |
| `severity` | `Warning` |
| `extensions` | `.anim` |
| `pathFilters` | `Assets/_Game/Animation/**` |
| `requiredPrefix` | `anim_` |
| `convention` | `SnakeCase` |
| `allowAutoFix` | on |
| `note` | `anim_<character>_<action>_<frame>. Padding keeps Run_10 sorting after Run_02.` |

| Before | After |
|---|---|
| `player run 2.anim` | `anim_player_run_02.anim` |
| `Player_Run_2.anim` | `anim_player_run_02.anim` |
| `anim_player_run_10.anim` | already correct |

The padding runs before the casing conversion, and both survive a second scan, so the report
settles at zero instead of reporting the same file forever.

### Variant B — `A_Player_Run_01` style

`PascalSnakeCase` handles this on its own. Each underscore-separated segment has to start with a
capital letter **or a digit**, so `Player_Run_01` satisfies it — the trailing `_01` is a legal
segment, not a violation. That matters: a convention that rejected `_01` would report the same clip
forever, because the fixer would hand back the name it had just rejected.

| Field | Value |
|---|---|
| `displayName` | `Animation clips` |
| `severity` | `Warning` |
| `extensions` | `.anim` |
| `pathFilters` | `Assets/_Game/Animation/**` |
| `requiredPrefix` | `A_` |
| `convention` | `PascalSnakeCase` |
| `note` | `A_<Character>_<Action>_<NN>. Padding keeps Run_10 sorting after Run_02.` |

With the same **Numbering** settings as variant A, this is fully auto-fixable:

| Before | After |
|---|---|
| `player run 1.anim` | `A_Player_Run_01.anim` |
| `Player_Run_2.anim` | `A_Player_Run_02.anim` |
| `A_Player_Run_10.anim` | already correct |

If you want the *shape* pinned down as well — exactly three segments, exactly two digits — add a
regex on top:

| Field | Value |
|---|---|
| `customRegex` | `^A_[A-Za-z0-9]+_[A-Za-z0-9]+_[0-9]{2}$` |
| `regexMustMatch` | on |

The prefix and the casing stay auto-fixable. The regex shape is reported only — the Cop will not
invent a name that satisfies an arbitrary pattern — so a clip called `A_Player_01` gets a finding
you have to resolve by hand.

---

## 8. Audio split into SFX, Music and VO

Four rules working together. The three folder rules score 12 and win inside the audio tree; the
stray-catcher scores 4 and picks up anything that landed elsewhere.

**Rule 1 — SFX**

| Field | Value |
|---|---|
| `displayName` | `Audio SFX` |
| `extensions` | `.wav, .ogg` |
| `pathFilters` | `Assets/_Game/Audio/SFX/**` |
| `requiredPrefix` | `SFX_` |
| `convention` | `PascalCase` |
| `severity` | `Warning` |

**Rule 2 — Music**

| Field | Value |
|---|---|
| `displayName` | `Audio Music` |
| `extensions` | `.wav, .ogg, .mp3` |
| `pathFilters` | `Assets/_Game/Audio/Music/**` |
| `requiredPrefix` | `MUS_` |
| `convention` | `PascalCase` |
| `severity` | `Warning` |

**Rule 3 — Voice over**

| Field | Value |
|---|---|
| `displayName` | `Audio VO` |
| `extensions` | `.wav, .ogg` |
| `pathFilters` | `Assets/_Game/Audio/VO/**` |
| `requiredPrefix` | `VO_` |
| `convention` | `SnakeCase` |
| `note` | `VO file names carry the line id, so snake_case keeps them readable: VO_hero_line_004.` |

**Rule 4 — stray audio anywhere else**

| Field | Value |
|---|---|
| `displayName` | `Stray audio` |
| `extensions` | `.wav, .ogg, .mp3, .aiff, .flac` |
| `pathFilters` | (leave empty) |
| `convention` | `Any` |
| `allowedFolders` | `Assets/_Game/Audio/**` |
| `preferredFolder` | `Assets/_Game/Audio` |
| `severity` | `Warning` |
| `note` | `Audio that is not under Audio/. Move it, then let the folder rules rename it.` |

Rule 4 deliberately uses `convention: Any`. Its job is placement, not casing. Once a stray is
moved into `Audio/SFX`, rule 1 takes over on the next scan and fixes the prefix.

---

## 9. Prefab variants

| Field | Value |
|---|---|
| `displayName` | `Prefab variants` |
| `severity` | `Info` |
| `extensions` | `.prefab` |
| `pathFilters` | `**/Variants/**` |
| `requiredSuffix` | `_Variant, _V` |
| `convention` | `PascalCase` |
| `allowAutoFix` | on |
| `note` | `A variant reads as a variant in the hierarchy without opening it.` |

`Enemy_Grunt.prefab` in a `Variants` folder becomes `EnemyGrunt_Variant.prefab`. The suffix is
stripped before the casing check, so the corrected name passes on the next scan instead of being
re-flagged.

Pair it with a general prefab rule, which scores 4 and therefore loses inside `Variants`:

| Field | Value |
|---|---|
| `displayName` | `Prefabs` |
| `extensions` | `.prefab` |
| `convention` | `PascalCase` |
| `allowedFolders` | `Assets/_Game/Prefabs/**` |
| `preferredFolder` | `Assets/_Game/Prefabs` |
| `severity` | `Warning` |

---

## 10. Localization CSVs

| Field | Value |
|---|---|
| `displayName` | `Localization tables` |
| `severity` | `Error` |
| `extensions` | `.csv` |
| `pathFilters` | `Assets/_Game/Localization/**` |
| `requiredPrefix` | `loc_` |
| `convention` | `Any` |
| `customRegex` | `^loc_[a-z]{2}(_[A-Z]{2})?$` |
| `regexMustMatch` | on |
| `minLength` | `6` |
| `maxLength` | `12` |
| `note` | `loc_<language> or loc_<language>_<REGION>. loc_en, loc_pt_BR. The importer keys off this.` |

`Error` is right here because a mis-named localization table is not an aesthetic problem — the
importer will not find it and a language silently disappears.

The regex runs against the whole file name with the extension removed, so it sees `loc_pt_BR`,
not `loc_pt_BR.csv`. A mismatch is reported, never auto-fixed. The missing `loc_` prefix is
auto-fixable.

---

## 11. Shader graphs

| Field | Value |
|---|---|
| `displayName` | `Shader graphs` |
| `severity` | `Warning` |
| `extensions` | `.shadergraph, .shadersubgraph` |
| `requiredPrefix` | `SG_` |
| `convention` | `PascalCase` |
| `allowedFolders` | `Assets/_Game/Art/Shaders/**` |
| `preferredFolder` | `Assets/_Game/Art/Shaders` |
| `note` | `SG_ for graphs, so a search for SG_ lists every shader we authored.` |

Renaming a shader file is safe in both directions that matter. Materials reference a shader by
GUID, so they follow the rename, and the shader's declared name lives inside the file and is not
touched, so `Shader.Find("MyGame/Water")` keeps working.

For hand-written shaders, add a second rule:

| Field | Value |
|---|---|
| `displayName` | `Hand-written shaders` |
| `extensions` | `.shader, .hlsl, .cginc` |
| `requiredPrefix` | `S_` |
| `convention` | `PascalCase` |
| `severity` | `Info` |

---

## 12. New assets only, using pathFilters

The single most useful configuration change on an existing project. Take any rule you already
have and scope it to where new work lands.

| Field | Value |
|---|---|
| `pathFilters` | `Assets/_Game/**` |

Useful scoping globs:

| Glob | Scopes the rule to |
|---|---|
| `Assets/_Game/**` | One product folder and everything under it |
| `Assets/_Game/Art/**` | One discipline |
| `**/UI/**` | Every folder called `UI`, wherever it is |
| `Assets/_Game/Levels/Level_*/**` | Per-level folders, but not the shared level assets |
| `Assets/_Game/**/Textures` | Every folder literally named `Textures` under the product folder |

**`pathFilters` and `ignoredPaths` are not the same tool.**

| | `pathFilters` on a rule | `ignoredPaths` on the rule set |
|---|---|---|
| Scope | That one rule | The whole scan |
| Project-wide checks still run? | Yes — illegal characters, duplicates, junk words, path length | No |
| Use it for | "This convention only applies to new work" | "This is third-party code, we will never rename it" |

There is no "not this folder" glob — `pathFilters` only ever says *include*. So for a legacy folder
you still want checked for spaces and duplicates, you point every rule's `pathFilters` at the
folders you *do* want, and simply leave the legacy one out; recipe 13 is the tidier way to do the
same thing with one rule instead of ten. A vendor plugin belongs in `ignoredPaths`, where nothing
looks at it at all.

---

## 13. A warn-only rule for a legacy folder

The other half of recipe 12: instead of scoping every rule away from the legacy folder, write one
rule that claims it.

| Field | Value |
|---|---|
| `displayName` | `Legacy art (do not touch)` |
| `enabled` | on |
| `severity` | `Info` |
| `pathFilters` | `Assets/Legacy/**` |
| `extensions` | (leave empty) |
| `convention` | `Any` |
| `minLength` | `0` |
| `maxLength` | `0` |
| `requiredPrefix` | (leave empty) |
| `allowAutoFix` | **off** |
| `note` | `Shipped in the 2023 build. Several of these are loaded with Resources.Load. Report only. Ask before renaming anything here.` |

This rule matches everything under `Assets/Legacy` and demands nothing. Because a path filter
scores 8 and a plain extension rule scores 4, it takes the folder away from your strict rules
without you having to edit any of them.

`minLength` and `maxLength` at `0` disable the length checks. `convention: Any` disables the
casing check. `allowAutoFix` off guarantees the rule never proposes a change.

The project-wide checks still apply to these assets, so illegal characters and duplicates are
still reported. If you want complete silence, add `Assets/Legacy/**` to `ignoredPaths` instead
and delete this rule.

---

## 14. Scenes

| Field | Value |
|---|---|
| `displayName` | `Scenes` |
| `severity` | `Warning` |
| `extensions` | `.unity` |
| `convention` | `PascalCase` |
| `allowedFolders` | `Assets/_Game/Scenes/**` |
| `preferredFolder` | `Assets/_Game/Scenes` |
| `minLength` | `4` |
| `allowAutoFix` | **off** |
| `note` | `Check for SceneManager.LoadScene("Name") before renaming. Build Settings follows the GUID; your code does not.` |

Unity's Build Settings scene list tracks scenes by GUID, so a rename or a move does not drop a
scene from the build. `SceneManager.LoadScene("MainMenu")` takes a name string and does not
follow. Leave `allowAutoFix` off, search the code, then rename by hand.

The same warning applies to anything under a `Resources` folder. Consider adding
`Assets/**/Resources/**` to `ignoredPaths`.

---

## 15. Catch duplicate names before they ship twice

Not a rule. Project-wide settings on the `NamingRuleSet`, under **Duplicates**.

| Field | Value |
|---|---|
| `checkDuplicateNames` | on |
| `duplicateScope` | `NameAndExtension` |
| `duplicateSeverity` | `Warning` |

| Scope | Two assets collide when | Noise level |
|---|---|---|
| `NameOnly` | Names match, even across types | High. `Player.cs` and `Player.prefab` legitimately share a name. |
| `NameAndExtension` | Names and extensions match | The sensible default. |
| `NameAndType` | Names match and both are the same Unity type | Lowest, but forces a type lookup on every asset. |

Duplicates are reported, never auto-fixed. The Cop cannot know which of `hero.png` and `hero.png`
your materials actually reference, and guessing would be worse than leaving it to you. Both the
first asset and every later clash are listed, each naming the other, so you can open them side by
side.

Raise this to `Error` once the report is clean. A duplicate texture is build size you pay for
forever.

---

## 16. Keep paths under the Windows limit

Not a rule either. Project-wide settings under **Path length**.

| Field | Value |
|---|---|
| `checkPathLength` | on |
| `maxPathLength` | `180` |
| `pathLengthSeverity` | `Warning` |

`maxPathLength` accepts 80 to 250. The default of 180 leaves room for a build machine whose
workspace folder is deeper than yours — the real 260 character wall is hit on their machine, not
on yours.

A path-length finding has no automatic fix, because the answer is almost always to shorten a
folder name and that is a decision, not a rename. To stop it happening again in one deep area,
cap the file names there:

| Field | Value |
|---|---|
| `displayName` | `Short names in deep folders` |
| `pathFilters` | `Assets/_Game/Levels/**/Props/**` |
| `maxLength` | `32` |
| `convention` | `PascalCase` |
| `severity` | `Info` |

The `maxLength` clamp trims the name body and never the prefix or suffix, so a `T_` prefix
survives the truncation.

---

## 17. Folder names, PascalCase

Not a rule. Project-wide settings under **Folders**.

| Field | Value |
|---|---|
| `checkFolderNaming` | on |
| `folderConvention` | `PascalCase` |
| `folderNamingSeverity` | `Info` |

Folder names appear in every asset path underneath them, so a space in one folder name is a space
in five hundred paths. This check is off by default because turning it on for the first time on an
existing project is exactly the kind of thing that produces a 400-row report.

Start at `Info`. Folder renames are the most disruptive change in this whole tool — everyone's
next pull is a large move — so schedule them, do not batch them.

Two related checks live one section up, under **Housekeeping**:

| Field | Default | What it does |
|---|---|---|
| `checkEmptyFolders` | on, `Info` | Flags folders with nothing in them or below them. Reported, never deleted. |
| `checkLooseRootAssets` | on, `Warning` | Flags assets sitting directly in `Assets/` rather than in a category folder. |

---

## Testing a recipe without touching a single file

Three ways, cheapest first.

**Scan a selection.** Select one folder in the Project window, then
`Assets ▸ Asset Naming Manager ▸ Check Selected`. The scan is read-only and covers only that
selection.

**Group the report by rule.** The Report tab's **Group by** already starts on `Rule`. You immediately see how
many findings your new rule produced, and whether another rule is claiming the assets you expected
it to own.

**Ask for one name in code.**

```csharp
using AssetNamingManager;
using UnityEditor;
using UnityEngine;

public static class RulePreview
{
    [MenuItem("Tools/My Studio/Preview Naming Rule")]
    public static void Preview()
    {
        var ruleSet = AssetDatabase.LoadAssetAtPath<NamingRuleSet>(
            "Assets/AssetNamingManager/Settings/NamingRules.asset");

        const string path = "Assets/_Game/Art/player run copy.png";
        string extension = NamingScanner.GetExtension(path);              // ".png"
        string fileName  = NamingScanner.GetFileNameWithoutExtension(path);

        NamingRule rule = ruleSet.ResolveRule(path, extension, "Texture2D");
        Debug.Log(rule == null
            ? "No rule claims this asset."
            : "Rule: " + rule.displayName + " (specificity " + rule.Specificity + ")");

        Debug.Log("Ideal name: " + NamingFixer.ComputeIdealName(ruleSet, rule, fileName));
    }
}
```

`ResolveRule` tells you which rule won. `ComputeIdealName` tells you what the fix would produce.
Neither touches disk.

---

## Validate before you commit

There is no Validate button to press. The Rules tab runs `NamingRuleSet.Validate()` every time it
draws and puts anything it finds in a **Configuration to look at** box above the preset bar, so a
broken rule is in front of you the moment you switch to that tab. You can also call
`NamingRuleSet.Validate()` yourself; it returns a plain `List<string>`.

It catches the four mistakes people actually make:

| Message | Fix |
|---|---|
| `Rule 'X' has an invalid regular expression.` | The pattern did not compile. The regex check is silently skipped until you fix it. |
| `Rule 'X' has a minimum length larger than its maximum.` | Nothing can satisfy the rule. |
| `Rule 'X' has a preferred folder but no allowed folders, so it will never move anything.` | Add `allowedFolders`. |
| `Two rules are both called 'X'. Reports will be confusing.` | Rename one. Reports and the Group-by-Rule view become unreadable otherwise. |

It also warns when no rule is set to `Error` while `ciFailOn` is `Error`, which means your CI gate
can never fail — and when `/` has found its way into `forbiddenCharacters`.

Read that box before you commit. A CI run treats **any** of these as a misconfigured job and exits
`2` rather than reporting on your naming, so a duplicate rule name is enough to redden a build.

---

## One more time: do not turn all of this on at once

Every recipe here is written at the severity a settled project would use. On day one, set them all
to `Info`, scope them with `pathFilters`, and raise them one at a time as each area gets clean.
A report with 400 red rows gets the tool turned off in week one.
