# Asset Naming Manager

One button. Every naming mistake in your project, with a safe one-click fix.

Asset Naming Manager is an editor-only Unity tool. It scans your project, tells you exactly which
file breaks which rule, and offers a reference-safe fix. Renames and moves go through
`AssetDatabase.MoveAsset`, so GUIDs survive and nothing in your prefabs, scenes or scripts breaks.

| | |
|---|---|
| Package root | `Assets/AssetNamingManager/` |
| Namespace | `AssetNamingManager` |
| Assembly | `AssetNamingManager.Editor` (Editor platform only) |
| Build size impact | None. Nothing ships in a player build. |
| Minimum Unity | 2022.3 LTS. Verified on 2022.3, Unity 6000.4, 6000.5, 6000.6 |
| Render pipeline | Irrelevant. Built-in, URP and HDRP all work. |
| Dependencies | None. Pure Unity API, no third-party packages. |

---

## The problem

A real project ends up with `player_run.png`, `PlayerIdle.PNG` and `Player Attack 02.png` all
meaning the same thing. Nobody wrote the convention down, so searching the Project window takes
three tries with three spellings, a new team member spends their first week asking where things
go, the same texture ships twice under two names, and six months in the `Assets/` root has forty
orphan files nobody owns. That is roughly ten to fifteen minutes a day lost to searching and
wrong-asset references, plus a duplicate tax on build size. None of it is hard to fix. It is just
that nobody has a list of what is broken, and renaming a hundred files by hand is a good way to
break a hundred references.

---

## Install

1. Import the package from the Asset Store. Everything lands in `Assets/AssetNamingManager/`.
2. Wait for the compile to finish. There is no setup wizard and nothing to configure first.
3. Open `Tools ▸ Asset Naming Manager ▸ Open Window`.

The package contains an Editor-only assembly definition. If your project uses assembly
definitions, nothing you own needs to reference it — the tool is driven from menus and from the
window. Only reference `AssetNamingManager.Editor` if you want to call the API from your own editor
scripts.

---

## 60-second quick start

1. `Tools ▸ Asset Naming Manager ▸ Open Window`.
2. The package already ships an active rule set at
   `Assets/AssetNamingManager/Settings/NamingRules.asset`, on the **Unity Standard** preset. To use a
   different convention, switch preset from the **Rules** tab.
3. Click **Scan**.
4. Read the count. If it is large, stop and read
   [Do not boil the ocean](#do-not-boil-the-ocean) before you fix anything.
5. Tick the rows you trust, click **Preview changes**, read the list, then apply it.
6. If you do not like the result, go to the **History** tab and click **Revert** on the batch.

Commit `NamingRules.asset`. That file is your team's convention, written down instead of living
in one person's head.

---

## Do not boil the ocean

This is the one piece of advice that decides whether the tool stays installed.

Never apply a brand-new convention to an entire legacy project on day one. Run the scan and look
at the count. If it says 400 violations, do one of these instead:

- Narrow the rule's `pathFilters` to the folders where new work happens, for example
  `Assets/_Game/**`.
- Drop the rule's `severity` to `Info` so it reports without nagging.
- Add the legacy folders to the rule set's `ignoredPaths`.

Then tighten it later, one folder at a time. A report with 400 red rows gets the tool turned off
in week one. A report with 12 rows gets fixed.

---

## The window

Open it with `Tools ▸ Asset Naming Manager ▸ Open Window`. The window is built with UI Toolkit and
follows the editor skin, Pro and Light.

### Header

The strip across the top, visible on every tab.

| Control | What it does |
|---|---|
| Rule set field | The `NamingRuleSet` this project scans with. Change it here to switch convention. |
| Theme | Swaps the window's colour tokens. A theme overrides colours only, so package updates still reach you. |
| Selection | Scans only what is selected in the Project window |
| Scan | Runs a full scan against the active rule set |

The line under the title is the last scan at a glance: assets scanned, findings, health score.

### Report tab

The list of findings. It is virtualized, so a report with thousands of rows still scrolls. Its own
toolbar sits above the list:

| Control | What it does |
|---|---|
| Search | Filters the visible rows by name, path, rule or message |
| Severity chips | Three counts — Errors, Warnings, Info. Click one to show or hide those rows. |
| Group by | None, Rule, Folder, Asset Type, Severity, Problem |
| Select fixable | Ticks every finding the Cop can correct on its own |
| Export | Writes the current report as CSV, JSON, Markdown or HTML |

- A severity pill on every row: blue for Info, amber for Warning, red for Error.
- Per-row before and after, with the part of the name that would change highlighted.
- A checkbox on each row for batch selection. Tick anything and a bar appears telling you how many
  renames, moves and new folders the selection adds up to, with **Preview changes** next to it.
  That opens a sheet listing every change, which you then **Apply** or cancel.
- Single click pings the asset in the Project window. Double click opens it.
- The details pane shows the long explanation of the problem and the team note from the rule that
  fired, when the rule has one, plus a **Fix this one** button for that single asset.
- After a batch is applied, an undo bar offers **Revert that batch** without leaving the tab.

One asset can produce several rows: wrong prefix *and* wrong folder are two findings. They are
merged into a single rename before anything is applied, so you never get two renames on one file.

### Dashboard tab

A read-only summary of the last scan.

- **Health Score**, 0 to 100, drawn as a Painter2D arc gauge.
- A severity donut: how the findings split across Info, Warning and Error.
- A bar chart of the top offending rules.
- A list of the worst folders.

The score is computed by `ScanReport.HealthScore`:

```
weighted = Errors * 3 + Warnings * 1 + Info * 0.35
score    = 100 * (1 - weighted / (assetsScanned + foldersScanned))
```

clamped to 0 to 100. A project with no findings scores 100. The one-word verdict next to it comes
from `ScanReport.HealthLabel`:

| Score | Label |
|---|---|
| No findings at all | Spotless |
| 95 and above | Healthy |
| 85 to 95 | Minor drift |
| 70 to 85 | Needs a pass |
| 50 to 70 | Messy |
| Below 50 | Critical |

The score is relative to project size. A hundred findings in a 20,000 asset project scores better
than a hundred findings in a 500 asset project, which is the behaviour you want from a headline
number.

### Rules tab

A readable view of the active rule set, as cards rather than as a raw inspector list. Every card
spells out what that rule matches, what it checks and — just as usefully — what it does *not*
check, so an empty allowed-folders list never reads as a folder check that happens to be passing.

The cards themselves are read-only. What you can act on here:

- The toggle on each card turns that rule off without deleting it.
- The preset bar switches the whole rule set to one of the shipped presets. The preset you are
  already on is marked and its button disabled. Switching rewrites the rules only — no asset is
  renamed or moved, and anything already renamed stays renamed.
- **Edit in Inspector** selects the rule set and pings it in the Project window.
- **Rescan** runs the scan again, for when you have just changed something.

Adding, duplicating, reordering and deleting rules, and editing any field on one, happens in the
normal Inspector on the `NamingRuleSet` asset. The cards report what that asset currently says.

Validation runs every time the tab is drawn, so `NamingRuleSet.Validate()` results are always in
front of you: an invalid regular expression, a minimum length larger than the maximum, two rules
with the same name, a preferred folder on a rule that has no allowed folders, and a CI gate that
can never fire all appear in a box above the cards.

### History tab

Every fix batch the Cop has applied, newest first, with the number of files it touched and a
**Revert** button. See [The Fix Journal](#the-fix-journal).

---

## How rules work

A rule says: *assets that look like this must be named like that and live over there.*

Rules live in a `NamingRuleSet` ScriptableObject. Commit it and the whole team shares one
convention.

### Matchers — which assets a rule looks at

All three are optional and are ANDed together. An empty matcher is a wildcard.

| Field | Type | Example | Meaning |
|---|---|---|---|
| `extensions` | comma-separated string | `.png, .jpg, .tga` | File extensions this rule covers. A leading dot is added if you leave it out. |
| `typeNames` | comma-separated string | `Texture2D, Sprite` | Unity type names, compared against the main asset type. |
| `pathFilters` | string array of globs | `Assets/_Game/Art/**` | Only look at assets whose path matches one of these. |

A rule with only `extensions` set applies to every asset with those extensions anywhere in the
project. A rule with no matchers at all applies to everything.

`pathFilters` decides **who the rule looks at**. It is not the same as `allowedFolders`, which
decides **where those assets must live**. Getting these two confused is the most common
configuration mistake:

- `pathFilters: Assets/UI/**` means "ignore everything outside `Assets/UI`". A stray sprite in
  `Assets/Art` is never even examined by this rule.
- `allowedFolders: Assets/UI/**` means "wherever I find one of these, it must be under
  `Assets/UI` or it is a violation".

### Glob syntax

`pathFilters`, `allowedFolders`, `forbiddenFolders` and the rule set's `ignoredPaths` all use the
same small glob engine (`GlobMatcher`).

| Pattern | Matches |
|---|---|
| `*` | Any run of characters except `/` |
| `**` | Any run of characters including `/` |
| `?` | Exactly one character except `/` |
| `**/` | Also matches zero folders, so `Assets/**/T_*.png` hits `Assets/T_a.png` |

A pattern with no wildcard at all is treated as a "contains this folder segment" test. Typing
`Art/Textures` matches `Assets/Game/Art/Textures/Hero/T_Hero.png`, which is what people expect
when they type a folder path into a rule. Matching is case-insensitive and `\` is normalised to
`/`.

### Checks — what the rule demands

| Field | Default | What it checks |
|---|---|---|
| `requiredPrefix` | empty | The name must start with one of these. Several alternatives allowed: `T_, TEX_`. Empty skips the check. |
| `requiredSuffix` | empty | The name must end with one of these, before the extension. |
| `convention` | `PascalCase` | Casing, checked on the **name body after the prefix and suffix are stripped off**. |
| `minLength` | `3` | Shortest acceptable name, prefix included. `0` disables. |
| `maxLength` | `64` | Longest acceptable name, prefix included. `0` disables. |
| `customRegex` | empty | A regular expression applied to the whole file name, extension excluded. |
| `regexMustMatch` | `true` | On: the name must match. Off: the name must **not** match. |
| `allowedFolders` | empty | Globs. The asset must be inside one of them. |
| `forbiddenFolders` | empty | Globs. The asset must never be inside one of them. |
| `preferredFolder` | empty | Where a folder violation offers to move the asset. Needs `allowedFolders` to be set as well. |
| `severity` | `Warning` | `Info`, `Warning` or `Error`. |
| `allowAutoFix` | `true` | Off: this rule reports but never proposes a rename or move. |
| `note` | empty | Team note, shown in the details pane under every finding from this rule. |
| `enabled` | `true` | Off: the rule is skipped entirely. |

Only the length check short-circuits: a name that is too short is reported as `TooShort` and the
maximum is not also checked. Everything else is independent, so one asset can be reported for a
missing prefix, wrong casing and a wrong folder at the same time.

### Casing conventions

| Convention | Example | Regular expression used |
|---|---|---|
| `Any` | anything goes | none |
| `PascalCase` | `PlayerRunCycle` | `^[A-Z0-9][a-zA-Z0-9]*$` |
| `camelCase` | `playerRunCycle` | `^[a-z0-9][a-zA-Z0-9]*$` |
| `snake_case` | `player_run_cycle` | `^[a-z0-9]+(_[a-z0-9]+)*$` |
| `SCREAMING_SNAKE_CASE` | `PLAYER_RUN_CYCLE` | `^[A-Z0-9]+(_[A-Z0-9]+)*$` |
| `kebab-case` | `player-run-cycle` | `^[a-z0-9]+(-[a-z0-9]+)*$` |
| `Pascal_Snake_Case` | `Player_Run_Cycle` | `^[A-Z0-9][a-zA-Z0-9]*(_[A-Z0-9][a-zA-Z0-9]*)*$` |

Every one of these accepts a digit where you might expect a letter, and that is deliberate. A digit
has no case, so a name that starts with one cannot break a casing rule: `3DGrid`, `2DSprite` and
`8BitFont` are real asset names, and rejecting them would mean reporting those files forever, since
no rewrite of `3DGrid` can ever start with an uppercase letter.

The convention is checked on the body, after the prefix and suffix are removed and leading or
trailing `_`, `-` and spaces are trimmed. `T_Player_Hero` with `requiredPrefix: T_` and
`convention: Pascal_Snake_Case` is checked as `Player_Hero`, which passes. The prefix is never
counted against you.

#### Trailing numbers and casing

This trips people up, so it is worth being explicit: `Run_01` **does** satisfy
`Pascal_Snake_Case`. Each underscore-separated segment is allowed to start with a digit, so `01` is
a perfectly good segment. That is not an accident either: rejecting `_01` would mean reporting
every numbered clip forever, because the check would turn the name down and the fixer would hand
back the same `Run_01` it had just rejected.

| Convention | `Run` plus a number becomes | Underscore survives |
|---|---|---|
| `PascalCase` | `Run01` | No |
| `camelCase` | `run01` | No |
| `snake_case` | `run_01` | Yes |
| `SCREAMING_SNAKE_CASE` | `RUN_01` | Yes |
| `kebab-case` | `run-01` | Yes |
| `Pascal_Snake_Case` | `Run_01` | Yes |

`A_Player_Run_01` therefore passes a rule with `requiredPrefix: A_` and
`convention: Pascal_Snake_Case`: the prefix is stripped and `Player_Run_01` is what gets checked.
Reach for `convention: Any` plus a `customRegex` only when you want to pin the exact shape — a
fixed number of segments, or exactly two digits on the end. See the animation-clip recipe in
`RuleCookbook.md`.

### Specificity — which rule wins

Exactly one rule is responsible for each asset. When several rules match, the most specific one
wins:

| Matcher populated | Points |
|---|---|
| `pathFilters` | 8 |
| `extensions` | 4 |
| `typeNames` | 4 |

A rule with a path filter and an extension scores 12 and beats a rule with only an extension,
which scores 4. Ties go to the rule that sits highest in the list, so ordering still matters when
two rules are equally specific.

This is how you carve out exceptions. A general "Textures" rule matching `.png` scores 4. A
"Legacy art, do not touch" rule with `pathFilters: Assets/Legacy/**` scores 8 and takes over for
everything in that folder.

The project-wide checks in the next section are not rules and are never overridden by
specificity. They run on every asset the scan sees.

---

## Project-wide checks

These live on the `NamingRuleSet` itself, not on individual rules, and need no rule to fire.

| Check | Rule set fields | Default | What it catches |
|---|---|---|---|
| Illegal characters | `forbiddenCharacters`, `illegalCharacterSeverity` | Error, `` !@#$%^&*()+=[]{};'",<>?`~`` and space | Spaces and symbols that break command-line tooling, asset bundle names and some version control clients |
| ASCII only | `asciiOnly` | on | Accented or non-Latin file names that break build farms |
| Duplicate names | `checkDuplicateNames`, `duplicateScope`, `duplicateSeverity` | Warning, `NameAndExtension` | The same name twice, which is how a texture quietly ships twice |
| Loose in Assets root | `checkLooseRootAssets`, `looseRootSeverity` | Warning | Orphan files dumped at the top level of `Assets/` |
| Empty folders | `checkEmptyFolders`, `emptyFolderSeverity` | Info | Leftovers from refactors that quietly differ per machine |
| Junk words | `checkRedundantTokens`, `redundantTokens`, `redundantTokenSeverity` | Warning | copy, final, new, old, temp, tmp, test, backup, bak, untitled, duplicate, dup, wip |
| Folder naming | `checkFolderNaming`, `folderConvention`, `folderNamingSeverity` | off | Folder names against a casing convention |
| Number padding | `checkNumberPadding`, `numberPaddingWidth`, `numberPaddingSeverity` | off, width 2 | `Run_2` when the project wants `Run_02` |
| Path length | `checkPathLength`, `maxPathLength`, `pathLengthSeverity` | Warning, 180 | Paths heading for the Windows 260 character wall |

### Scope

| Field | Default | Meaning |
|---|---|---|
| `scanRoots` | `Assets` | Folders the scan walks. The single biggest lever on scan speed in a large project. |
| `ignoredPaths` | see below | Paths the scan skips, as globs. |

The default ignore list:

```
Assets/AssetNamingManager/Editor/**
Assets/AssetNamingManager/Documentation/**
Assets/AssetNamingManager/Settings/**
Assets/Plugins/**
Assets/ThirdParty/**
Assets/StreamingAssets/**
**/Editor Default Resources/**
**.dll
**.asmdef
**.asmref
```

Ten entries. Imported plugins and marketplace art do not follow your convention and should never
appear in your report. Add to this list freely.

`Assets/AssetNamingManager/Demo/**` is deliberately missing from it. The demo ships with badly named
sample files on purpose, so the first scan has something to show you. Delete that folder, or add
it to this list, once you have seen the tool work.

### Duplicate scope

| Value | Two assets collide when |
|---|---|
| `NameOnly` | Their file names match, even across different types |
| `NameAndExtension` | Their names and extensions match. The sensible default. |
| `NameAndType` | Their names match and both are the same Unity type |

`NameAndType` forces a type lookup on every asset in the scan, which is slower than the other two
scopes. So does any rule that uses `typeNames`.

### Notes on the junk-word check

A single-word name that *is* a junk word is left alone. A scene called `Test` is usually
deliberate, so the check only fires when junk hitch-hikes on an otherwise real name:
`Player_Test_01` is flagged, `Test` is not. The check needs at least two words in the name.

---

## How safe fixing works

### The ideal name

`NamingFixer.ComputeIdealName` builds the name the asset *should* have, in this order:

1. Strip characters banned by `forbiddenCharacters`, and non-ASCII characters when `asciiOnly` is
   on. Each removed character becomes `_`, then runs of separators collapse: `Player__Run--01_`
   becomes `Player_Run-01`.
2. Drop junk words, if `checkRedundantTokens` is on. A trailing `copy`, `(2) copy`, `kopie` or
   `copia` is removed as well.
3. Split off the prefix and suffix. If the name already uses one of the accepted prefixes, that
   one is kept. Otherwise the first entry in `requiredPrefix` is used.
4. Pad the trailing number to `numberPaddingWidth`, if `checkNumberPadding` is on.
5. Convert the body to the rule's `convention`.
6. Reattach the prefix and suffix.
7. Clamp to `maxLength` by trimming the body, never the prefix or suffix.

`player run copy.png` under a rule with `requiredPrefix: T_` and `convention: PascalCase` becomes
`T_PlayerRun.png`.

Acronyms are preserved. `UI`, `HUD`, `FX` and `LOD` are not re-cased into `Ui` or `Hud` when the
target is PascalCase, and they are lowered whole in camelCase so the name stays readable.
`Run02` tokenizes as `Run` plus `02`, `HTTPServer` as `HTTP` plus `Server`, and `2D` stays fused.

### Applying

1. Every finding on the same asset is merged into one `FixPlan`. Wrong prefix and wrong folder
   become a single rename-and-move, not two operations.
2. Plans that would not actually change anything are dropped.
3. Missing destination folders are created, one level at a time.
4. If the target path is already taken — including by another plan in the same batch, and
   including a case-only clash, because Windows and macOS file systems collide on case alone —
   the name gets a `_01`, `_02` counter until a free slot is found.
5. Every change is validated with `AssetDatabase.ValidateMoveAsset` before it is attempted.
6. The change is applied with `AssetDatabase.MoveAsset`.
7. The batch is recorded in the Fix Journal.

Anything that fails is counted in `FixResult.failed` and the reason is added to
`FixResult.problems`, ready to print. The rest of the batch still goes through.

### Why GUIDs survive

Unity does not identify an asset by its path. It identifies it by a GUID stored in the `.meta`
file next to it. Every reference in a prefab, a scene, an animator or a serialized field is a
GUID plus a local file id, not a file name.

`AssetDatabase.MoveAsset` moves the asset **and** its `.meta` file together, keeping the GUID.
That is why a rename through the Cop is safe and a rename in Windows Explorer is not: Explorer
leaves the `.meta` behind, Unity sees a new file, generates a new GUID, and every reference to
the old one turns into a missing reference.

The same applies to moves between folders. `Assets/Art/hero.png` moved to
`Assets/_Game/Art/Textures/T_Hero.png` keeps the same GUID, so the material that used it still
uses it.

Two things GUID preservation cannot save you from:

- **Scripts.** Renaming a `.cs` file does not rename the class inside it. C# requires them to
  match for a `MonoBehaviour`, so the rename compiles into an error. Set `allowAutoFix` to `false`
  on any rule that covers `.cs`.
- **Anything that looks assets up by string.** `Resources.Load("Art/hero")`,
  `AssetBundle.LoadAsset("hero")` and Addressables addresses that were left at their default path
  are path-based, not GUID-based. Search your code for the name before you rename an asset that
  is loaded at runtime.

---

## The Fix Journal

Unity's own undo stack does not cover `AssetDatabase` moves. Without a journal, a bulk rename of
two hundred files would be a one-way door.

Every batch the Cop applies is written to:

```
UserSettings/AssetNamingManager/FixJournal.json
```

- The last **20 batches** are kept. Older ones roll off.
- Each batch records the id, the UTC timestamp, and every `from` and `to` pair.
- `UserSettings/` is already excluded by Unity's default `.gitignore`, so the journal never
  pollutes a commit. It is also per-machine: your teammate cannot revert your batch.

Revert from the **History** tab, or in code:

```csharp
using System.Collections.Generic;
using AssetNamingManager;
using UnityEngine;

var batch = FixJournal.Latest();
if (batch != null)
{
    var problems = new List<string>();
    int restored = FixJournal.Revert(batch, problems);
    Debug.Log("Restored " + restored + " of " + batch.entries.Count + " assets.");
    foreach (var problem in problems) Debug.LogWarning(problem);
}
```

A revert walks the batch backwards, newest change first, recreates the original folder if it has
since been deleted, and moves each asset back. Files that were renamed again by hand after the
batch cannot be found and are reported in `problems` rather than guessed at. A batch that reverts
at least one asset is removed from the history.

---

## Presets

Load a preset from the **Rules** tab. A preset fills in a complete set of rules that you then edit
in the Inspector — it is a starting point, not a locked configuration. The rule set that ships with
the package is on **Unity Standard**.

| Preset | Use it when |
|---|---|
| **Unity Standard** | You want the conventions most Unity projects drift towards anyway. PascalCase everywhere, no prefixes. The shipped default. |
| **Unreal Style** | Your team came from Unreal, or you already use `T_` for textures, `M_` for materials, `SM_` for static meshes. |
| **Mobile 2D** | A 2D project where sprites, atlases and UI dominate. |
| **Minimal** | Illegal characters and duplicates only. The least intrusive way to start on a legacy project. |
| **Strict Studio** | Prefixes, folders and casing enforced at Error severity. For a new project with a team that has agreed on this up front. |

On a legacy project, start with **Minimal**. On a new project, start with whatever matches the
convention you already argue about in code review.

---

## Integrations

### Main menu

| Menu item | What it does |
|---|---|
| `Tools ▸ Asset Naming Manager ▸ Open Window` | Opens the main window |
| `Tools ▸ Asset Naming Manager ▸ Scan Project` | Opens the window and runs a full scan straight away. Shortcut `Ctrl+Shift+N`, `Cmd+Shift+N` on macOS |
| `Tools ▸ Asset Naming Manager ▸ Create Rule Set ▸ ‹preset›` | Creates a second `NamingRuleSet` asset from that preset and makes it active |

You can also create a rule set with `Assets ▸ Create ▸ Asset Naming Manager ▸ Naming Rule Set`.

There is no menu item for scanning part of the project. Use the **Selection** button in the window
header, or `Assets ▸ Asset Naming Manager ▸ Check Selected` from the Project window's right-click menu.

### Project window context menu

Right-click a selection in the Project window:

| Item | What it does |
|---|---|
| `Assets ▸ Asset Naming Manager ▸ Check Selected` | Scans just that selection |
| `Assets ▸ Asset Naming Manager ▸ Fix Selected` | Scans and fixes just that selection |

This is the safest way to adopt a convention on a legacy project: fix one folder, commit, move on.

### Project window overlay

A small coloured dot is drawn on every offending asset directly in the Project browser, so you
see the problem where you already are rather than only in the report. The colour follows the
severity. Toggle it in Project Settings.

### Project Settings

`Edit ▸ Project Settings ▸ Asset Naming Manager`, in two halves.

**Rule set**, which your team shares: pick the active one, and toggle the import check. That check
is `checkOnImport` on the rule set asset, so it travels with the asset.

**Preferences**, this machine only: the Project window overlay, scanning automatically when the
window opens, printing every finding to the console, and asking before a batch of fixes is applied.
One button resets all four.

Which rule set is active is itself a per-machine preference. It is kept in `EditorPrefs` under a
key derived from the project's own path, so it is per checkout as well and never reaches a commit.
That is rarely something to think about: with a single `NamingRuleSet` in the project the tool
finds it on its own, and only a project carrying several of them needs anybody to choose.

### Import hook

An `AssetPostprocessor` that checks assets the moment they are imported and warns in the console,
so a badly named file gets caught when it arrives rather than six months later. Off by default;
turn it on with `checkOnImport` on the rule set or from Project Settings.

It runs only on what was just imported, so it costs nothing on a project-wide reimport of assets
that are already fine.

### CI

See `CI-Integration.md` for the full setup. The short version:

```bash
Unity -batchmode -quit -projectPath . \
  -executeMethod AssetNamingManager.NamingManagerCI.Run \
  -namingManagerReport Reports/naming.html \
  -namingManagerFailOn error
```

Exit code 0 means clean, 1 means the gate failed, 2 means the run was misconfigured — no rule set,
a rule set that does not pass `Validate()`, an argument it could not read, or a report path it
could not write. A misconfigured run never quietly reports itself as clean.

Both switches are optional. Without `-namingManagerFailOn` the rule set's own `ciFailOn` decides, so
the gate travels with the convention instead of living in the job's YAML. It takes `info`,
`warning`, `error` or `none`, naming the **lowest** severity that fails the build, so `error` is
the loosest gate and `none` publishes a report without ever blocking a merge. Without
`-namingManagerReport` the report is a timestamped HTML file in `reportFolder`; with one, the format
comes from the file extension.

The gate is report-only by design. Nothing in a CI run renames or moves an asset — a rename is a
decision, so CI reports it and a person presses the button.

---

## Report formats

Export from the toolbar, or call `ReportExporter.Write(report, format, path)`.

| Format | `ReportFormat` | Good for |
|---|---|---|
| CSV | `ReportFormat.Csv` | Dropping into a spreadsheet, sorting by folder, assigning owners |
| JSON | `ReportFormat.Json` | Feeding another tool or a dashboard |
| Markdown | `ReportFormat.Markdown` | Pasting into a pull request, an issue or a wiki page |
| HTML | `ReportFormat.Html` | A self-contained styled page. Publish it as a CI build artifact and link it from the build. |

The HTML report is a single file with no external assets, so it works as a build artifact, as an
email attachment and offline.

`NamingRuleSet.reportFolder` (default `NamingManagerReports`) is where a CI run drops its report when
the command line does not name a path: `NamingManagerCI` writes a timestamped HTML file there. It is
relative to the project root, next to `Assets`, deliberately outside `Assets` so Unity does not
re-import your reports and then complain about their names. The folder is created on the way.

Exporting from the window does not use it. That opens a save dialog and remembers the folder you
last saved to, on your machine only.

---

## Public API

Everything below lives in the `AssetNamingManager` namespace, in the `AssetNamingManager.Editor` assembly.
Reference that assembly from your own editor assembly definition to use it.

### Entry points

| Member | Signature | Purpose |
|---|---|---|
| `NamingScanner.Scan` | `static ScanReport Scan(NamingRuleSet ruleSet, ScanOptions options = null)` | Read-only scan. Never touches an asset. |
| `NamingFixer.ComputeIdealName` | `static string ComputeIdealName(NamingRuleSet ruleSet, NamingRule rule, string fileName)` | The name an asset should have. Pass `null` for `rule` to apply only the project-wide cleanup. |
| `NamingFixer.BuildPlans` | `static List<FixPlan> BuildPlans(IEnumerable<AssetNamingViolation> violations)` | Merges findings per asset into concrete changes. |
| `NamingFixer.Apply` | `static FixResult Apply(IList<FixPlan> plans, bool showProgress = true)` | Applies changes. The only code that writes. |
| `NamingFixer.Apply` | `static FixResult Apply(IEnumerable<AssetNamingViolation> violations, bool showProgress = true)` | Plan and apply in one call. |
| `NamingFixer.EnsureFolder` | `static bool EnsureFolder(string folder)` | Creates a folder chain such as `Assets/Art/Textures/UI`. |
| `ReportExporter.Write` | `static string Write(ScanReport report, ReportFormat format, string path)` | Writes a report to disk and returns the absolute path it wrote. |

### `NamingRuleSet` — the config asset

ScriptableObject. Create with `Assets ▸ Create ▸ Asset Naming Manager ▸ Naming Rule Set`.

| Member | Type | Default |
|---|---|---|
| `scanRoots` | `string[]` | `{ "Assets" }` |
| `ignoredPaths` | `string[]` | ten entries, see above |
| `rules` | `List<NamingRule>` | empty |
| `forbiddenCharacters` | `string` | `` " !@#$%^&*()+=[]{};'\",<>?`~" `` |
| `asciiOnly` | `bool` | `true` |
| `illegalCharacterSeverity` | `NamingSeverity` | `Error` |
| `checkDuplicateNames` | `bool` | `true` |
| `duplicateScope` | `DuplicateScope` | `NameAndExtension` |
| `duplicateSeverity` | `NamingSeverity` | `Warning` |
| `checkLooseRootAssets` | `bool` | `true` |
| `looseRootSeverity` | `NamingSeverity` | `Warning` |
| `checkEmptyFolders` | `bool` | `true` |
| `emptyFolderSeverity` | `NamingSeverity` | `Info` |
| `checkRedundantTokens` | `bool` | `true` |
| `redundantTokens` | `string[]` | `NameUtility.DefaultRedundantTokens` |
| `redundantTokenSeverity` | `NamingSeverity` | `Warning` |
| `checkFolderNaming` | `bool` | `false` |
| `folderConvention` | `NamingConvention` | `PascalCase` |
| `folderNamingSeverity` | `NamingSeverity` | `Info` |
| `checkNumberPadding` | `bool` | `false` |
| `numberPaddingWidth` | `int`, range 2 to 5 | `2` |
| `numberPaddingSeverity` | `NamingSeverity` | `Info` |
| `checkPathLength` | `bool` | `true` |
| `maxPathLength` | `int`, range 80 to 250 | `180` |
| `pathLengthSeverity` | `NamingSeverity` | `Warning` |
| `checkOnImport` | `bool` | `false` |
| `ciFailOn` | `NamingSeverity` | `Error` |
| `reportFolder` | `string` | `"NamingManagerReports"` |
| `version` | `int` | `NamingRuleSet.CurrentVersion` (1) |

| Method | Returns | Purpose |
|---|---|---|
| `ResolveRule(string assetPath, string extension, string typeName)` | `NamingRule` | The rule responsible for an asset, or `null`. Most specific wins. |
| `IsIgnored(string assetPath)` | `bool` | True when the path matches an ignore glob. |
| `ResolvedScanRoots()` | `string[]` | Scan roots normalised, empties dropped, falls back to `Assets`. |
| `Validate()` | `List<string>` | Configuration mistakes. An empty list means the rule set is fine. |

### `NamingRule` — one rule row

Plain `[Serializable]` class, not a ScriptableObject.

| Field | Type | Default |
|---|---|---|
| `displayName` | `string` | `"New Rule"` |
| `enabled` | `bool` | `true` |
| `severity` | `NamingSeverity` | `Warning` |
| `note` | `string` | empty |
| `extensions` | `string` | empty |
| `typeNames` | `string` | empty |
| `pathFilters` | `string[]` | empty |
| `requiredPrefix` | `string` | empty |
| `requiredSuffix` | `string` | empty |
| `convention` | `NamingConvention` | `PascalCase` |
| `minLength` | `int` | `3` |
| `maxLength` | `int` | `64` |
| `customRegex` | `string` | empty |
| `regexMustMatch` | `bool` | `true` |
| `allowedFolders` | `string[]` | empty |
| `forbiddenFolders` | `string[]` | empty |
| `preferredFolder` | `string` | empty |
| `allowAutoFix` | `bool` | `true` |

| Member | Type | Purpose |
|---|---|---|
| `Extensions` | `string[]` | `extensions` split and lower-cased, dots added |
| `TypeNames` | `string[]` | `typeNames` split |
| `Prefixes` | `string[]` | `requiredPrefix` split. Any one satisfies the rule. |
| `Suffixes` | `string[]` | `requiredSuffix` split |
| `CompiledRegex` | `Regex` | `customRegex` compiled, or `null` when empty or invalid |
| `Specificity` | `int` | 8 for path filters, 4 for extensions, 4 for types |
| `AppliesTo(string assetPath, string extension, string typeName)` | `bool` | True when this rule is responsible for the asset |
| `MatchedPrefix(string name)` | `string` | The prefix the name already uses, or `null` |
| `MatchedSuffix(string name)` | `string` | The suffix the name already uses, or `null` |
| `Validate(List<string> problems)` | `void` | Appends this rule's configuration mistakes |
| `Clone()` | `NamingRule` | Deep copy, arrays included |

### `ScanOptions`

| Field | Type | Default | Purpose |
|---|---|---|---|
| `roots` | `string[]` | `null` | Folders to walk. `null` falls back to the rule set's `scanRoots`. |
| `explicitPaths` | `string[]` | `null` | When set, only these exact paths are examined and `roots` is ignored. |
| `includeFolders` | `bool` | `true` | Run the folder checks. |
| `includeDuplicates` | `bool` | `true` | Run the cross-project duplicate pass. |
| `onProgress` | `Func<float, string, bool>` | `null` | `(progress 0-1, status) => keepGoing`. Return `false` to cancel. |

### `ScanReport`

| Member | Type | Purpose |
|---|---|---|
| `violations` | `List<AssetNamingViolation>` | Every finding, in scan order |
| `assetsScanned` | `int` | Non-folder assets examined |
| `foldersScanned` | `int` | Folders examined |
| `assetsIgnored` | `int` | Skipped by an ignore glob |
| `durationSeconds` | `double` | Wall-clock scan time |
| `ruleSetName` | `string` | Name of the rule set that produced the report |
| `timestampUtc` | `DateTime` | When the scan ran |
| `cancelled` | `bool` | True when the user cancelled, so the numbers are partial |
| `ErrorCount` / `WarningCount` / `InfoCount` | `int` | Findings per severity, cached |
| `AffectedAssetCount` | `int` | Distinct assets with at least one finding |
| `AutoFixableCount` | `int` | Findings with a concrete change ready |
| `HealthScore` | `float` | 0 to 100 |
| `HealthLabel` | `string` | Spotless, Healthy, Minor drift, Needs a pass, Messy, Critical |
| `WorstSeverity` | `NamingSeverity?` | Highest severity present, `null` when clean |
| `FailsGate(NamingSeverity gate)` | `bool` | True when this report should fail a CI gate at that level |
| `CountBy(Func<AssetNamingViolation, string> selector)` | `Dictionary<string, int>` | Counts by any field |
| `ScanReport.Sorted(Dictionary<string, int> map, int take)` | `List<KeyValuePair<string, int>>` | Descending, capped. Static. |
| `InvalidateCounts()` | `void` | Call after mutating `violations` directly |
| `Summary()` | `string` | One-line summary for the console |

### `AssetNamingViolation`

| Member | Type | Purpose |
|---|---|---|
| `assetPath` | `string` | Unity-style path |
| `guid` | `string` | Stable key, survives the rename |
| `assetName` | `string` | File name without extension |
| `extension` | `string` | Lower-case, dot included. Empty for folders. |
| `typeName` | `string` | Short Unity type name, or `Folder` |
| `kind` | `ViolationKind` | What went wrong |
| `severity` | `NamingSeverity` | How loudly to complain |
| `ruleName` | `string` | Rule that fired. Empty for project-wide checks. |
| `message` | `string` | One-line summary |
| `detail` | `string` | Long explanation, with the rule's team note appended |
| `suggestedName` | `string` | Proposed name, or `null` |
| `suggestedFolder` | `string` | Proposed folder, or `null` |
| `IsRename` / `IsMove` / `CanAutoFix` | `bool` | What applying the fix would do |
| `Directory` | `string` | Current folder |
| `ResolvedPath` | `string` | Path the asset would have after the fix |
| `KindLabel` | `string` | Human label, e.g. `Wrong casing` |
| `AssetNamingViolation.Describe(ViolationKind)` | `string` | Human label for any kind. Static. |

### `FixPlan` and `FixResult`

| `FixPlan` member | Type | Purpose |
|---|---|---|
| `sourcePath` | `string` | Where the asset lives today |
| `targetName` | `string` | New name, or `null` to keep it |
| `targetFolder` | `string` | New folder, or `null` to keep it |
| `extension` | `string` | Carried over from the source |
| `reasons` | `List<string>` | The problems this one change resolves |
| `nameWasDeduplicated` | `bool` | Set when a collision forced a `_01` counter |
| `TargetPath` | `string` | Path after the plan is applied |
| `IsMeaningful` | `bool` | False when the plan would change nothing |

| `FixResult` member | Type | Purpose |
|---|---|---|
| `renamed` / `moved` / `skipped` / `failed` | `int` | Totals |
| `cancelled` | `bool` | True when the progress bar was cancelled |
| `batchId` | `string` | Pass to `FixJournal.Find` to revert this batch |
| `problems` | `List<string>` | Per-asset failure messages, ready to print |
| `Changed` | `int` | `renamed + moved` |
| `Summary()` | `string` | One-line summary |

### `FixJournal`

| Member | Returns | Purpose |
|---|---|---|
| `All()` | `List<Batch>` | Every recorded batch, newest last |
| `Latest()` | `Batch` | Most recent batch, `null` when nothing has been fixed |
| `Find(string id)` | `Batch` | Look a batch up by `FixResult.batchId` |
| `Record(List<Entry> entries)` | `string` | Stores a batch, returns its id |
| `Revert(Batch batch, List<string> problems = null, bool showProgress = true)` | `int` | Assets restored |
| `Clear()` | `void` | Deletes the history. Files stay where they are. |

`FixJournal.Entry` has `from` and `to`. `FixJournal.Batch` has `id`, `timestampUtc`, `entries`
and a `DisplayTime` label such as `14 Mar 2026, 16:20`.

### `NameUtility` — pure helpers

| Member | Returns | Purpose |
|---|---|---|
| `DefaultRedundantTokens` | `string[]` | The 13 shipped junk words |
| `Tokenize(string name)` | `List<string>` | Splits on separators, camel humps, acronym runs and letter-to-digit boundaries |
| `Matches(string body, NamingConvention convention)` | `bool` | Does this body already follow the convention |
| `Example(NamingConvention convention)` | `string` | Short human example, e.g. `PlayerRunCycle` |
| `Convert(string body, NamingConvention convention)` | `string` | Rewrite so `Matches` returns true |
| `Capitalize(string token)` / `Decapitalize(string token)` | `string` | Acronym-aware case change |
| `StripIllegalCharacters(string name, string forbidden, bool asciiOnly, char replacement, out bool removed)` | `string` | Clean and collapse separators |
| `FindIllegalCharacter(string name, string forbidden, bool asciiOnly)` | `string` | The first banned character, or `null` |
| `CollapseSeparators(string name)` | `string` | `Player__Run--01_` becomes `Player_Run-01` |
| `FindRedundantToken(string name, IList<string> tokens)` | `string` | The junk word found, or `null` |
| `RemoveRedundantTokens(string name, IList<string> tokens)` | `string` | Name with junk words removed |
| `FindUnderPaddedNumber(string name, int width)` | `string` | The under-padded trailing digits, or `null` |
| `PadTrailingNumber(string name, int width)` | `string` | `Run_1` at width 2 becomes `Run_01` |
| `CommonPrefixLength(string a, string b)` | `int` | Used to highlight the diff in the UI |
| `SplitList(string value)` | `string[]` | Splits on `,` `|` `;` and trims |

### `GlobMatcher`

| Member | Returns | Purpose |
|---|---|---|
| `NormalizePath(string path)` | `string` | `\` to `/`, trailing slash removed |
| `IsMatch(string path, string pattern)` | `bool` | Single pattern test |
| `IsMatchAny(string path, IList<string> patterns)` | `bool` | True when any pattern matches |
| `Translate(string pattern)` | `string` | The anchored regular expression a glob compiles to |
| `ClearCache()` | `void` | Drops the compiled-pattern cache |

### `NamingScanner` path helpers

`GetExtension(string path)`, `GetFileName(string path)`,
`GetFileNameWithoutExtension(string path)` and `GuessTypeLabel(string extension)` are public and
safe on any Unity asset path.

### Enums

| Enum | Values |
|---|---|
| `NamingSeverity` | `Info` = 0, `Warning` = 1, `Error` = 2 |
| `NamingConvention` | `Any`, `PascalCase`, `CamelCase`, `SnakeCase`, `ScreamingSnakeCase`, `KebabCase`, `PascalSnakeCase` |
| `ViolationKind` | `MissingPrefix`, `MissingSuffix`, `ConventionMismatch`, `IllegalCharacter`, `WrongFolder`, `ForbiddenFolder`, `DuplicateName`, `TooLong`, `TooShort`, `RegexMismatch`, `RedundantToken`, `NumberPadding`, `LooseInRoot`, `EmptyFolder`, `FolderNaming`, `PathTooLong` |
| `DuplicateScope` | `NameOnly`, `NameAndExtension`, `NameAndType` |
| `GroupMode` | `None`, `Rule`, `Folder`, `AssetType`, `Severity`, `Problem` |
| `ReportFormat` | `Csv`, `Json`, `Markdown`, `Html` |

### Example: scan one folder from your own menu item

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

public static class ArtNamingCheck
{
    const string RuleSetPath = "Assets/AssetNamingManager/Settings/NamingRules.asset";

    [MenuItem("Tools/My Studio/Check Art Naming")]
    public static void CheckArt()
    {
        var ruleSet = AssetDatabase.LoadAssetAtPath<NamingRuleSet>(RuleSetPath);
        if (ruleSet == null)
        {
            Debug.LogError("No rule set at " + RuleSetPath);
            return;
        }

        var options = new ScanOptions
        {
            roots = new[] { "Assets/_Game/Art" },
            includeDuplicates = false,
            onProgress = (progress, status) =>
                !EditorUtility.DisplayCancelableProgressBar("Checking art", status, progress)
        };

        ScanReport report = NamingScanner.Scan(ruleSet, options);
        EditorUtility.ClearProgressBar();

        Debug.Log(report.Summary());
        foreach (var violation in report.violations)
            Debug.Log(violation, AssetDatabase.LoadMainAssetAtPath(violation.assetPath));
    }
}
```

### Example: preview changes, then apply only what you want

```csharp
using System.Collections.Generic;
using AssetNamingManager;
using UnityEditor;
using UnityEngine;

public static class TextureNameFix
{
    public static void FixTexturesOnly(NamingRuleSet ruleSet)
    {
        ScanReport report = NamingScanner.Scan(ruleSet);

        var selected = new List<AssetNamingViolation>();
        foreach (var violation in report.violations)
        {
            if (violation.ruleName == "Textures" && violation.CanAutoFix)
                selected.Add(violation);
        }

        List<FixPlan> plans = NamingFixer.BuildPlans(selected);
        foreach (var plan in plans)
            Debug.Log(plan.sourcePath + "  ->  " + plan.TargetPath);

        if (!EditorUtility.DisplayDialog("Asset Naming Manager",
                "Apply " + plans.Count + " changes?", "Apply", "Cancel"))
            return;

        FixResult result = NamingFixer.Apply(plans);
        Debug.Log(result.Summary() + " Batch id: " + result.batchId);
        foreach (var problem in result.problems) Debug.LogWarning(problem);
    }
}
```

### Example: preview one name without scanning

```csharp
var ruleSet = AssetDatabase.LoadAssetAtPath<NamingRuleSet>(
    "Assets/AssetNamingManager/Settings/NamingRules.asset");

NamingRule rule = ruleSet.ResolveRule(
    "Assets/Art/player run copy.png", ".png", "Texture2D");

string ideal = NamingFixer.ComputeIdealName(ruleSet, rule, "player run copy");
// With requiredPrefix "T_" and convention PascalCase this is "T_PlayerRun".
```

---

## FAQ

**Will this break references in my prefabs and scenes?**
No. Every change goes through `AssetDatabase.MoveAsset`, which moves the `.meta` file with the
asset and keeps the GUID. References are stored as GUIDs, so they follow the asset. The two
exceptions are C# scripts, where the class name must match the file name, and anything loaded by
path string such as `Resources.Load` or a default Addressables address.

**Can I undo a fix?**
Yes, from the **History** tab. Unity's Ctrl+Z does not cover `AssetDatabase` moves, which is why
the tool keeps its own journal at `UserSettings/AssetNamingManager/FixJournal.json`. The last 20
batches are revertible. Beyond that, your version control is the backup.

**Does anything ship in my build?**
No. The assembly is Editor-only. There is no runtime code, no resources folder, no scriptable
singleton loaded at startup. The build size impact is zero bytes.

**My scan found 600 violations. What now?**
Do not fix them. Read [Do not boil the ocean](#do-not-boil-the-ocean). Narrow `pathFilters` to
the folders where new work happens, or drop the severity to `Info`, or add the legacy folders to
`ignoredPaths`. Then fix one folder at a time with `Assets ▸ Asset Naming Manager ▸ Fix Selected`.

**Two rules both match one of my assets. Which one applies?**
Exactly one. The most specific rule wins: path filters score 8, extensions 4, type names 4, and
the scores add up. A tie goes to whichever rule is higher in the list. This is how you write a
general rule and then override it for one folder.

**Why is my prefix not being counted as part of the casing check?**
Because it should not be. The convention is checked on the name body after the prefix and suffix
are stripped and leading or trailing separators are trimmed. `T_Player_Hero` with
`requiredPrefix: T_` is checked as `Player_Hero`. Otherwise every prefixed name would fail
PascalCase forever.

**Does `Run_01` pass `Pascal_Snake_Case`?**
Yes. A segment may start with a digit, so `01` is a valid segment and the underscore survives an
automatic fix. `snake_case`, `SCREAMING_SNAKE_CASE` and `kebab-case` keep a trailing `_01` too.
Use `Any` plus a `customRegex` only when you need to pin the exact shape rather than the casing.

**What happens when two assets want the same corrected name?**
The first one gets the name. The rest get `_01`, `_02` and so on, up to `_99`, and the plan is
marked with `nameWasDeduplicated`. The comparison is case-insensitive on purpose, because Windows
and macOS file systems collide on case alone.

**Does the tool delete anything?**
Never. It only renames and moves. Empty folders are reported, not removed. Duplicate assets are
reported, not deleted — the Cop cannot know which of the two you actually reference.

**Can I have a rule that reports but never changes anything?**
Yes. Set `allowAutoFix` to `false` on the rule. Its own checks will report with no proposed
rename. Note that the project-wide checks — illegal characters, junk words, number padding — are
not part of any rule and can still propose a cleaned name for that asset.

**A scene called `Test` is not flagged as a junk word. Is that a bug?**
No, it is deliberate. A single-word name that *is* the junk word is usually intentional. The
check needs at least two words, so `Player_Test_01` is flagged and `Test` is not.

**Is the scan slow on a big project?**
The dominant costs are the number of assets walked and whether a type lookup is needed. Keep
`scanRoots` tight, keep `ignoredPaths` populated, and avoid `typeNames` matchers and
`DuplicateScope.NameAndType` unless you need them — both force
`AssetDatabase.GetMainAssetTypeAtPath` on every asset. The glob engine caches its compiled
patterns.

**Can the whole team share one convention?**
Yes, that is the point of the rule set being an asset. Commit
`Assets/AssetNamingManager/Settings/NamingRules.asset` and everyone scans against the same rules.
Which rule set is *active* is not part of that: it is remembered per machine and per checkout in
`EditorPrefs`, so it cannot be committed and does not need to be — with one rule set in the
project it is discovered automatically, and only a project with several needs each person to pick.
The Fix Journal is deliberately not shared either — it lives in `UserSettings/` and is per-machine.

**Can I run it in CI?**
Yes. `AssetNamingManager.NamingManagerCI.Run` via `-executeMethod`, which writes a report and exits
non-zero when the gate fails. See `CI-Integration.md`.

**Does it work with URP or HDRP?**
It does not care. The tool never touches rendering. It reads asset paths and names.

**Does it handle folders as well as files?**
Yes. Folders are checked for emptiness, for the folder casing convention when
`checkFolderNaming` is on, and for illegal characters. A space in a folder name ends up in every
asset path underneath it.

**Can I use my own junk-word list?**
Yes. `redundantTokens` on the rule set is a plain string array, pre-filled with the 13 defaults.
Your team probably has its own words. Add `wip2`, `andys`, `dontuse` or whatever yours are.

---

## Troubleshooting

**"This rule set has no rules."**
Validation is telling you the rule set is empty. Open the **Rules** tab and load a preset, or add
a rule by hand in the Inspector.

**"Rule 'X' has a preferred folder but no allowed folders, so it will never move anything."**
`preferredFolder` is the destination for a folder violation, and a folder violation can only
happen when `allowedFolders` or `forbiddenFolders` is set. Add the allowed folders.

**"No rule is set to Error, so a CI gate set to 'Error' can never fail."**
Either raise a rule to `Error`, raise `illegalCharacterSeverity` to `Error`, or lower `ciFailOn`
to `Warning`. As written, your CI gate is a no-op.

**"Rule 'X' has an invalid regular expression."**
`customRegex` did not compile. An invalid expression is not thrown at scan time — the rule simply
skips its regex check — so fix it before you rely on it. Note that `\` in a rule field is a
literal backslash: write `\d{2}`, not `\\d{2}`.

**A rule reports the same file every scan and Fix does nothing.**
The Cop has no better name to offer, so there is nothing to apply. A `customRegex` mismatch is the
usual cause: the Cop can tell you that a name does not match a pattern, but it cannot invent a name
that would. Those rows read "fix by hand" and their checkbox is disabled. Rename the file yourself,
or express the requirement as a prefix, suffix and convention, which the fixer can build.

**A rule I expected to fire is silent.**
Check in this order:

1. `enabled` is on.
2. The asset is not covered by `ignoredPaths`, and is inside `scanRoots`.
3. The `extensions` value matches the real extension, lower-case, dot included.
4. `pathFilters` actually matches the asset's path. Remember it restricts *what the rule looks
   at*, not where the asset must live.
5. A more specific rule is not claiming the asset first. Group the report by Rule to see which
   rule owns it.

**A rule fires on assets I never meant it to cover.**
Its matchers are too loose. A rule with an empty `extensions`, `typeNames` and `pathFilters`
applies to every asset in the project. Add at least one matcher.

**"Scan root does not exist and was skipped."**
A path in `scanRoots` is not a real folder. Check the spelling, and check that it starts with
`Assets`.

**Some assets failed during a fix.**
Read `FixResult.problems`, which the window prints to the console. The usual causes are a file
locked by another program, a version control provider with the file checked out elsewhere, or a
path that `AssetDatabase.ValidateMoveAsset` rejected. The rest of the batch still applied and is
in the journal.

**A revert says "not found, it may have been renamed again since."**
The asset moved after the batch was recorded, so the journal's `to` path no longer exists. The
Cop refuses to guess. Find it manually, or roll back in version control.

**"Could not write the fix journal."**
The `UserSettings/AssetNamingManager` folder is not writable. Fixes still apply, but they will not be
revertible from the History tab. Check folder permissions and whether `UserSettings/` is
read-only in your checkout.

**Renaming a script broke my build.**
Renaming a `.cs` file does not rename the class inside it. Set `allowAutoFix` to `false` on any
rule that covers `.cs`, and revert the batch from the History tab.

**An asset loaded by `Resources.Load` stopped resolving.**
`Resources.Load` takes a path string, not a GUID, so it does not follow a rename. Search your
code for the old name, or exclude `Assets/**/Resources/**` in `ignoredPaths`.

---

## Support

Questions, bug reports and feature requests: **Chaturvedisuryadev1@gmail.com**

When reporting a problem, please include:

- Your Unity version and the operating system.
- The exported report, HTML or JSON, if the issue is with what was or was not found.
- Your `NamingRules.asset`, or a screenshot of the rule that is behaving unexpectedly.
- The console output, including any `[Asset Naming Manager]` lines.

The rest of the documentation lives in this folder: `QuickStart.md` for a guided first run,
`RuleCookbook.md` for copy-paste rule recipes, `CI-Integration.md` for the pull-request gate, and
`CHANGELOG.md` for the release history.
