From 81d925332a5a19e43ae2464065278f1f660e10ac Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Apr 2026 07:34:49 +0000 Subject: [PATCH 01/21] chore(skills): scaffold directory structure for overhaul - Add references/ and scripts/ subfolders under each skill - Create directories for unity-ci-validation, unity-package-bootstrap, repo-conventions - Archive pre-overhaul SKILL.md content under .claude/skills/_archive/ --- .../migration-and-refactor-planner.pre-overhaul.md | 8 ++++++++ .claude/skills/_archive/repo-auditor.pre-overhaul.md | 8 ++++++++ .../_archive/unity-assetdatabase-tools.pre-overhaul.md | 8 ++++++++ .../skills/_archive/unity-editor-tooling.pre-overhaul.md | 8 ++++++++ .../_archive/unity-package-architect.pre-overhaul.md | 8 ++++++++ .../_archive/unity-package-reviewer.pre-overhaul.md | 8 ++++++++ .../_archive/unity-serialized-workflow.pre-overhaul.md | 8 ++++++++ .../_archive/unity-undo-prefab-safety.pre-overhaul.md | 8 ++++++++ 8 files changed, 64 insertions(+) create mode 100644 .claude/skills/_archive/migration-and-refactor-planner.pre-overhaul.md create mode 100644 .claude/skills/_archive/repo-auditor.pre-overhaul.md create mode 100644 .claude/skills/_archive/unity-assetdatabase-tools.pre-overhaul.md create mode 100644 .claude/skills/_archive/unity-editor-tooling.pre-overhaul.md create mode 100644 .claude/skills/_archive/unity-package-architect.pre-overhaul.md create mode 100644 .claude/skills/_archive/unity-package-reviewer.pre-overhaul.md create mode 100644 .claude/skills/_archive/unity-serialized-workflow.pre-overhaul.md create mode 100644 .claude/skills/_archive/unity-undo-prefab-safety.pre-overhaul.md diff --git a/.claude/skills/_archive/migration-and-refactor-planner.pre-overhaul.md b/.claude/skills/_archive/migration-and-refactor-planner.pre-overhaul.md new file mode 100644 index 00000000..1e9bd8ba --- /dev/null +++ b/.claude/skills/_archive/migration-and-refactor-planner.pre-overhaul.md @@ -0,0 +1,8 @@ +--- +name: migration-and-refactor-planner +description: Используй при планировании многошаговых рефакторингов — декомпозиция монолитных файлов, смена минимальной версии Unity, переименование namespace, добавление Tests~/Documentation~/Samples~. +--- + +# Migration & Refactor Planner + +Плейбуки: декомпозиция монолитов (по блоку за коммит), добавление тестов (Tests/Editor + asmdef + smoke), извлечение в UPM (структура + namespace + git URL), смена версии Unity, ренейм namespace. Каждый шаг = отдельный коммит. diff --git a/.claude/skills/_archive/repo-auditor.pre-overhaul.md b/.claude/skills/_archive/repo-auditor.pre-overhaul.md new file mode 100644 index 00000000..539e241a --- /dev/null +++ b/.claude/skills/_archive/repo-auditor.pre-overhaul.md @@ -0,0 +1,8 @@ +--- +name: repo-auditor +description: Используй при запросе аудита, сканирования или отчёта о здоровье Unity UPM-пакета. Только отчёт, без правок. +--- + +# Repo Auditor + +Сканируй репо и выдавай таблицу OK/WARN/FAIL по: package.json, asmdef, структура, namespace, размеры файлов, README/CHANGELOG, LICENSE, CI, антипаттерны. Не пиши фиксы. diff --git a/.claude/skills/_archive/unity-assetdatabase-tools.pre-overhaul.md b/.claude/skills/_archive/unity-assetdatabase-tools.pre-overhaul.md new file mode 100644 index 00000000..1e724a63 --- /dev/null +++ b/.claude/skills/_archive/unity-assetdatabase-tools.pre-overhaul.md @@ -0,0 +1,8 @@ +--- +name: unity-assetdatabase-tools +description: Используй при написании AssetPostprocessor, батчинге AssetDatabase-операций с StartAssetEditing, управлении импортом или генерации HideAndDontSave-ассетов. +--- + +# AssetDatabase Tools + +Батчинг: StartAssetEditing в try/finally. AssetPostprocessor: GetPostprocessOrder явно + bypass HashSet. Progress bar: DisplayCancelableProgressBar + ClearProgressBar в finally. ЗАПРЕЩЕНО: StartAssetEditing без try/finally, Refresh внутри цикла, Resources.Load в Editor, FindAssets без t: фильтра. diff --git a/.claude/skills/_archive/unity-editor-tooling.pre-overhaul.md b/.claude/skills/_archive/unity-editor-tooling.pre-overhaul.md new file mode 100644 index 00000000..9b78fa77 --- /dev/null +++ b/.claude/skills/_archive/unity-editor-tooling.pre-overhaul.md @@ -0,0 +1,8 @@ +--- +name: unity-editor-tooling +description: Используй при создании EditorWindow, MenuItem, [InitializeOnLoad], [OnOpenAsset], AssetModificationProcessor или других точек входа Unity Editor. +--- + +# Unity Editor Tooling + +Точки входа: EditorWindow (GetWindow/CreateInstance), AssetModificationProcessor (OnWillSaveAssets, IsOpenForEdit), [OnOpenAsset], [InitializeOnLoad]. Запреты: ручной JSON, UnityWebRequest без timeout, статическое состояние без контракта, тяжёлые операции в OnGUI, Editor API в Runtime asmdef. diff --git a/.claude/skills/_archive/unity-package-architect.pre-overhaul.md b/.claude/skills/_archive/unity-package-architect.pre-overhaul.md new file mode 100644 index 00000000..35e5e300 --- /dev/null +++ b/.claude/skills/_archive/unity-package-architect.pre-overhaul.md @@ -0,0 +1,8 @@ +--- +name: unity-package-architect +description: Используй при проектировании или исправлении package.json, asmdef, структуры Editor/Runtime/Tests~/Samples~/Documentation~/Native~, или ограничений версии Unity. +--- + +# Unity Package Architect + +Эталон: package.json (com.company.name, semver, LTS unity, нет нестандартных полей), asmdef (Company.Pkg.Editor, includePlatforms:["Editor"]), Editor/Runtime/Tests~/Documentation~/Samples~/Native~. Нейминг: Company.PackageName. Для монорепо: git URL + ?path=subfolder. diff --git a/.claude/skills/_archive/unity-package-reviewer.pre-overhaul.md b/.claude/skills/_archive/unity-package-reviewer.pre-overhaul.md new file mode 100644 index 00000000..cf24039b --- /dev/null +++ b/.claude/skills/_archive/unity-package-reviewer.pre-overhaul.md @@ -0,0 +1,8 @@ +--- +name: unity-package-reviewer +description: Используй при ревью диффа, PR или существующего файла на нарушения правил Unity-пакета. Указывает конкретный скилл, чьё правило нарушено. +--- + +# Unity Package Reviewer + +CRITICAL: .mesh, DestroyImmediate без Undo, мутация prefab без scope, StartAssetEditing без try/finally, мутация без Undo, target cast, Editor в Runtime. HIGH: ручной JSON, нет timeout, забытый ApplyModifiedProperties, нет bypass, >50КБ, хардкод. LOW: нет #if, нет progress bar, namespace без префикса. diff --git a/.claude/skills/_archive/unity-serialized-workflow.pre-overhaul.md b/.claude/skills/_archive/unity-serialized-workflow.pre-overhaul.md new file mode 100644 index 00000000..214c06bd --- /dev/null +++ b/.claude/skills/_archive/unity-serialized-workflow.pre-overhaul.md @@ -0,0 +1,8 @@ +--- +name: unity-serialized-workflow +description: Используй при работе с SerializedObject/SerializedProperty, создании CustomEditor/PropertyDrawer или сравнении сериализованных значений. +--- + +# Unity Serialized Workflow + +Цикл: Update() → FindProperty → ApplyModifiedProperties(). ЗАПРЕЩЕНО: мутация target cast. BeginProperty/EndProperty в PropertyDrawer обязательны. Read-only доступ через new SerializedObject(obj).FindProperty(path). diff --git a/.claude/skills/_archive/unity-undo-prefab-safety.pre-overhaul.md b/.claude/skills/_archive/unity-undo-prefab-safety.pre-overhaul.md new file mode 100644 index 00000000..e1767980 --- /dev/null +++ b/.claude/skills/_archive/unity-undo-prefab-safety.pre-overhaul.md @@ -0,0 +1,8 @@ +--- +name: unity-undo-prefab-safety +description: Используй при мутации prefab-ассетов, записи Undo-групп, редактировании prefab-оверрайдов или клонировании asset-backed мешей перед модификацией. +--- + +# Undo и Prefab Safety + +Undo-группы: SetCurrentGroupName → GetCurrentGroup → CollapseUndoOperations. RAII: UndoGroupScope. Prefab: EditPrefabContentsScope. Temp-instance: InstantiatePrefab → try {SaveAsPrefabAsset} finally {DestroyImmediate}. Оверрайды: Get/SetPropertyModifications. Mesh: только .sharedMesh + клон. ЗАПРЕЩЕНО: .mesh, LoadAssetAtPath→мутация→SaveAssets, DestroyImmediate без Undo. From dfd91f87187640870c3851f0142b86b8fdf1b3cd Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Apr 2026 07:37:28 +0000 Subject: [PATCH 02/21] docs(skills): add canonical _shared/ references (naming, version gates, anti-patterns) --- .claude/skills/_shared/anti-patterns.md | 149 ++++++++++++++++--- .claude/skills/_shared/naming-conventions.md | 91 ++++++++++- .claude/skills/_shared/version-gates.md | 126 +++++++++++++--- 3 files changed, 319 insertions(+), 47 deletions(-) diff --git a/.claude/skills/_shared/anti-patterns.md b/.claude/skills/_shared/anti-patterns.md index a8f04c92..0fd29716 100644 --- a/.claude/skills/_shared/anti-patterns.md +++ b/.claude/skills/_shared/anti-patterns.md @@ -1,22 +1,127 @@ -# Антипаттерны Unity Editor-пакетов - -Консолидированный справочник запретов. Каждый пункт ссылается на скилл с правильным паттерном. - -## CRITICAL -1. `.mesh` вместо `.sharedMesh` — unity-undo-prefab-safety -2. Мутация prefab через LoadAssetAtPath — unity-undo-prefab-safety -3. `StartAssetEditing` без try/finally — unity-assetdatabase-tools -4. Мутация без Undo — unity-undo-prefab-safety -5. Прямая мутация target в CustomEditor — unity-serialized-workflow -6. Editor-код в Runtime asmdef — unity-package-architect -7. `DestroyImmediate` без Undo — unity-undo-prefab-safety - -## HIGH -8. Ручной JSON-парсинг — unity-editor-tooling -9. `UnityWebRequest` без timeout — unity-editor-tooling -10. `AssetPostprocessor` без bypass — unity-assetdatabase-tools -11. `Resources.Load` в Editor — unity-assetdatabase-tools -12. `Refresh()` внутри цикла — unity-assetdatabase-tools -13. Захардкоженные пути/URL — unity-editor-tooling -14. Забытый `ApplyModifiedProperties` — unity-serialized-workflow -15. Забытый `ClearProgressBar` — unity-assetdatabase-tools +# Anti-patterns (canonical) + +Every entry is a symptom, a root cause, and a fix. This file is referenced as Level-3 material by every skill in this directory. Keep entries short; deep-dive explanations belong in the relevant SKILL.md. + +## Undo and prefab pitfalls + +**1. `Undo.RecordObject` on reparenting silently fails.** +Root cause: `RecordObject` only records serialized property deltas; transform parent changes require `RegisterFullObjectHierarchyUndo` or `Undo.SetTransformParent`. +Fix: use `Undo.SetTransformParent(child, newParent, "Reparent")` for parenting operations. + +**2. `AddComponent()` recorded with `RecordObject` is lost on undo.** +Root cause: `RecordObject` does not capture component creation. +Fix: use `Undo.AddComponent(gameObject)`; it records the creation atomically. + +**3. Prefab instance override changes are not serialized.** +Root cause: `RecordObject` alone marks the object dirty but does not mark the override on the prefab instance. +Fix: call `Undo.RecordObject(obj, name)` first, mutate, then `PrefabUtility.RecordPrefabInstancePropertyModifications(obj)` — both calls, in that order, for every instance edit. + +**4. `DestroyImmediate` inside a batch is unundoable.** +Root cause: `Object.DestroyImmediate` bypasses the undo system. +Fix: use `Undo.DestroyObjectImmediate(obj)` inside the editor; reserve raw `DestroyImmediate` for EditorWindow teardown or asset cleanup where undo does not apply. + +**5. `RegisterCreatedObjectUndo` called after subsequent `RecordObject` entries loses the prior state.** +Root cause: the create-undo entry is inserted at the current group head; later `RecordObject` entries are orphaned across the group boundary. +Fix: call `Undo.RegisterCreatedObjectUndo(obj, name)` immediately after creation, before any further modification. + +## AssetDatabase pitfalls + +**6. `File.Move` / `File.Delete` on a Unity asset orphans the `.meta` file and breaks GUID links.** +Root cause: GUID-to-path resolution uses the sibling `.meta` file, which direct filesystem ops leave behind. +Fix: always use `AssetDatabase.MoveAsset`, `AssetDatabase.RenameAsset`, `AssetDatabase.CopyAsset`, `AssetDatabase.DeleteAsset`. + +**7. `StartAssetEditing` without paired `StopAssetEditing` locks the editor.** +Root cause: the counter is reference-counted; unreleased increments prevent the asset pipeline from draining. +Fix: wrap every `StartAssetEditing` in `try/finally`; on Unity 6 prefer `using (new AssetDatabase.AssetEditingScope())`. + +**8. Querying `AssetDatabase.LoadAssetAtPath` between Start/Stop returns stale or null.** +Root cause: imports are deferred until `StopAssetEditing`; query APIs see pre-batch state. +Fix: do all queries before entering the batch, or split the batch around the query. + +**9. `AssetDatabase.Refresh()` inside a loop.** +Root cause: `Refresh` triggers a full import scan; nesting multiplies the cost quadratically. +Fix: call `Refresh` once after the loop, or wrap the loop in `StartAssetEditing`/`StopAssetEditing`. + +**10. `AssetPostprocessor` without a recursion guard.** +Root cause: the postprocessor can cause re-imports that re-enter itself. +Fix: use a static `HashSet` keyed on asset path, populated in `try`, cleared in `finally`. + +## Serialization pitfalls + +**11. `serializedObject.Update()` called mid-modification discards unapplied changes.** +Root cause: `Update()` copies the target back into the serialized object, overwriting in-progress edits. +Fix: call `Update()` exactly once at the top of `OnInspectorGUI` and `ApplyModifiedProperties()` exactly once at the bottom. + +**12. `ApplyModifiedProperties` forgotten.** +Root cause: serialized property writes remain in the in-memory SerializedObject but never reach the target. +Fix: end every `OnInspectorGUI` and every `PropertyDrawer.OnGUI` with `ApplyModifiedProperties()`, or `EndProperty()` for drawers. + +**13. Direct mutation of `target` / `targets` inside a CustomEditor.** +Root cause: direct field writes bypass undo and prefab override recording. +Fix: always edit via `serializedObject.FindProperty(...)` and `ApplyModifiedProperties()`. + +**14. `MeshFilter.mesh` in edit mode leaks a cloned mesh every access.** +Root cause: `mesh` auto-clones the shared asset for the caller; the clone is never destroyed. +Fix: use `sharedMesh` in editor code; if you must clone, capture the result, use it, then `Object.DestroyImmediate(clone)` when done. + +**15. Editing `sharedMesh` mutates the asset for all instances.** +Root cause: `sharedMesh` is the authored asset; modifications persist across the project. +Fix: `var clone = Object.Instantiate(filter.sharedMesh); clone.name = "…_Edit"; // edit clone`. + +## Packaging pitfalls + +**16. `Samples/` without a trailing tilde ships into consumer compilation.** +Root cause: folders without `~` suffix are imported by AssetDatabase into the consumer project. +Fix: rename to `Samples~` and declare entries in the `samples` array of `package.json`. + +**17. Asmdef with both `includePlatforms` and `excludePlatforms` populated.** +Root cause: Unity requires exactly one of the two to be non-empty; the other must be `[]`. +Fix: pick one and clear the other. + +**18. Editor asmdef referenced from a Runtime asmdef.** +Root cause: Runtime code compiled for standalone player cannot resolve Editor symbols. +Fix: move the shared code to a third Runtime asmdef that both reference, or gate Editor-only members with `#if UNITY_EDITOR` inside a Runtime-safe file. + +**19. `"unity": "2022.3"` bump without updating CI matrix.** +Root cause: CI still validates against an unreachable minimum. +Fix: update the GameCI matrix and regenerate license activation for the new minimum in the same PR. + +**20. Missing `.meta` files in git.** +Root cause: `.gitignore` excludes `.meta` patterns inadvertently. +Fix: always commit every `.meta` file next to its asset; audit `.gitignore` for broad patterns like `*.meta`. + +## Skill-authoring pitfalls + +**21. Description shorter than 80 characters.** +Root cause: terse descriptions undertrigger the skill. +Fix: include what the skill does plus a `Use when …` clause naming file extensions, API names, and domain nouns. + +**22. First-person or second-person voice in description (`I can …`, `You should …`).** +Root cause: violates Anthropic's frontmatter best practice for skill descriptions. +Fix: rewrite in third person imperative (`Processes …`, `Audits …`). + +**23. Frontmatter keys outside the allowed set (`version`, `keywords`, `tags`, `author`).** +Root cause: Claude.ai rejects unknown keys at validation time. +Fix: keep frontmatter to `name`, `description`, `license`, `allowed-tools`, `metadata`, plus Claude Code extensions (`when_to_use`, `paths`, `model`, `effort`, `argument-hint`, `disable-model-invocation`, `user-invocable`, `context`, `agent`, `hooks`, `shell`). + +**24. SKILL.md body over 500 lines.** +Root cause: Anthropic's 5,000-token Level-2 budget is exceeded; auto-compaction truncates the body. +Fix: split overflow content into `references/` files and link from SKILL.md one level deep. + +**25. Windows backslash paths in skill text.** +Root cause: breaks path resolution on Linux/macOS CI and in `raw.githubusercontent.com` URLs. +Fix: always use forward slashes; add a CI check that greps for `\\[A-Za-z]` patterns inside `.claude/skills/**`. + +**26. Prettier reflows single-line YAML descriptions into folded scalars.** +Root cause: Prettier wraps long YAML string values; the parser then reads a truncated prefix. +Fix: add `.claude/skills/**/SKILL.md` to `.prettierignore` or pin each description with `# prettier-ignore`. + +**27. Time-sensitive language (`Before August 2025 use old API`).** +Root cause: the statement goes stale; Claude keeps citing outdated guidance. +Fix: move version-scoped text into collapsible `
` blocks keyed by Unity version or package version. + +## Further reading + +- `_shared/naming-conventions.md` +- `_shared/version-gates.md` +- `_checklists/undo-safety.md`, `_checklists/prefab-safety.md`, `_checklists/batch-safety.md`, `_checklists/package-release.md` diff --git a/.claude/skills/_shared/naming-conventions.md b/.claude/skills/_shared/naming-conventions.md index db3f9712..1e4eb46c 100644 --- a/.claude/skills/_shared/naming-conventions.md +++ b/.claude/skills/_shared/naming-conventions.md @@ -1,13 +1,88 @@ -# Naming Conventions +# Naming conventions (canonical) -## Namespace: `Company.PackageName` -Хорошо: `SashaRX.PrefabDoctor`. Плохо: `PrefabDoctor`. +Canonical naming rules for every Unity UPM package under the `SashaRX` umbrella. This file is a Level-3 reference for the skills listed at the bottom. Per-repo deviations are recorded in each repo's `repo-conventions/SKILL.md` and take precedence over this file where explicitly documented. -## package.json: `com.company.package-name` (kebab-case) +## Package identifier (reverse-DNS, lowercase) -## asmdef: `Company.PackageName.Editor` +The `name` field in `package.json` is a reverse-DNS identifier, all lowercase, using hyphens to separate words in the leaf segment. The leaf segment must match the repository folder name when both are lowercased. -## Файлы: PascalCase, один класс = один файл -EditorWindow → `*Window.cs`, PropertyDrawer → `*Drawer.cs`, Tests → `*Tests.cs` +- Canonical: `com.sasharx.` — e.g., `com.sasharx.prefabdoctor`, `com.sasharx.unitymeshlab` +- Prohibited: mixed case (`com.SashaRX.PrefabDoctor`), underscores (`com.sasharx.prefab_doctor`), missing vendor segment (`sasharx.prefabdoctor`) -## MenuItem: `Tools/Package Name/Action` +Rationale: Unity's Package Manager is case-sensitive on disk on Linux and inside tarballs. `raw.githubusercontent.com` URLs that UPM resolves are also case-sensitive. + +## Namespace (two segments, `SashaRX.`) + +Every namespace block in `.cs` files under `Editor/`, `Runtime/`, and `Tests/` must begin with `SashaRX.`. The `` segment is PascalCase and matches the repository folder name. + +- Canonical: `namespace SashaRX.PrefabDoctor`, `namespace SashaRX.UnityMeshLab.Editor`, `namespace SashaRX.UnityMeshLab.Tests` +- Prohibited: single-segment bare namespaces (`namespace LightmapUvTool`), three-or-more-segment vendor prefixes (`Com.SashaRX.PrefabDoctor`), arbitrary English words as root (`MyTools.PrefabDoctor`) + +Rationale: single-segment namespaces collide with C# type names in IntelliSense, conflict with `using` aliases, and violate the reverse-DNS-analog convention used by every public Unity UPM package (UniTask, R3, NaughtyAttributes, MessagePipe, VContainer). A repository currently using a bare namespace must migrate via `migration-and-refactor-planner` before merging new code. + +Sub-namespaces extend the two-segment root: `SashaRX..Editor`, `SashaRX..Runtime.Data`, `SashaRX..Tests.Editor`. + +## Asmdef name and `rootNamespace` + +Every assembly definition file follows the same two-segment rule, with optional suffixes that describe platform or role. + +| Role | Asmdef name | `rootNamespace` | `includePlatforms` | +|---|---|---|---| +| Runtime | `SashaRX.` | `SashaRX.` | `[]` (all platforms) | +| Editor | `SashaRX..Editor` | `SashaRX..Editor` | `["Editor"]` | +| Tests (Editor) | `SashaRX..Tests.Editor` | `SashaRX..Tests` | `["Editor"]` | +| Tests (Runtime) | `SashaRX..Tests.Runtime` | `SashaRX..Tests` | `[]` | + +The asmdef `name` field and the asmdef file name (without `.asmdef`) must be identical. The `rootNamespace` field must match the `namespace` block of every `.cs` file under that asmdef. + +## Folder layout + +``` +/ +├── package.json +├── README.md +├── CHANGELOG.md +├── LICENSE +├── Editor/ +│ ├── SashaRX..Editor.asmdef +│ └── /*.cs +├── Runtime/ +│ ├── SashaRX..asmdef +│ └── /*.cs +├── Tests/ +│ ├── Editor/ +│ │ └── SashaRX..Tests.Editor.asmdef +│ └── Runtime/ +│ └── SashaRX..Tests.Runtime.asmdef +├── Samples~/ (tilde-hidden; UPM-imported into consumer Assets/) +├── Documentation~/ (tilde-hidden; ignored by AssetDatabase) +└── .github/workflows/ +``` + +Rules: + +- PascalCase for every folder that contains `.cs` files. +- Tilde suffix (`~`) on `Samples~`, `Documentation~`, and `Tests~` when tests are shipped but excluded from consumer compilation. No `.meta` file is generated for tilde-hidden folders. +- Never place editor-only code under `Runtime/`. The asmdef platform filter is the enforcement boundary. + +## File naming + +- One public type per `.cs` file; file name equals the public type name (`PrefabHealthWindow.cs`). +- Partial classes: `PrefabHealthWindow.Toolbar.cs`, `PrefabHealthWindow.Data.cs`. +- Editor windows end in `Window`; property drawers end in `Drawer`; custom editors end in `Editor`; tests end in `Tests`. +- Interfaces begin with `I` (`IUvTool`, `IScannable`). + +## MenuItem and asset paths + +- MenuItem roots follow `Tools//` — e.g., `Tools/Prefab Doctor/Open Health Window`. +- `[CreateAssetMenu]` paths follow `Create//`. +- Asset paths in strings always use forward slashes, never backslashes, regardless of host OS. + +## Deviation protocol + +Any repository-specific deviation from the rules above must be documented in `repo-conventions/SKILL.md` with an explicit rationale. Deviations without a documented rationale are treated as defects. When a repo's `repo-conventions/SKILL.md` and this file conflict, the per-repo file wins. + +## Further reading + +- Every skill in this directory links to this file from its **Further reading** section. +- Related Level-3 references: `_shared/version-gates.md`, `_shared/anti-patterns.md`. diff --git a/.claude/skills/_shared/version-gates.md b/.claude/skills/_shared/version-gates.md index 5e8f0dc1..eab97ac9 100644 --- a/.claude/skills/_shared/version-gates.md +++ b/.claude/skills/_shared/version-gates.md @@ -1,26 +1,118 @@ -# Version Gates +# Version gates (canonical) + +Unity API availability thresholds used by every skill in this directory. Use this file as the single source of truth when gating code with `#if` directives or asmdef `versionDefines`. Target the minimum Unity version declared in `package.json` (`unity` field); anything newer requires an explicit gate. + +## `UNITY_X_Y_OR_NEWER` preprocessor directives + +| Directive | First Unity version | Enables | +|---|---|---| +| `UNITY_2020_1_OR_NEWER` | 2020.1 | `PrefabUtility.EditPrefabContentsScope` (IDisposable prefab asset edit) | +| `UNITY_2021_2_OR_NEWER` | 2021.2 | `PrefabStage` moved to `UnityEditor.SceneManagement`; `OnPostprocessAllAssets(…, bool didDomainReload)` overload | +| `UNITY_2021_3_OR_NEWER` | 2021.3 LTS | Baseline floor for most SashaRX packages | +| `UNITY_2022_2_OR_NEWER` | 2022.2 | UI Toolkit-first inspectors (`CreateInspectorGUI`) reach maturity | +| `UNITY_2023_1_OR_NEWER` | 2023.1 | Awaitable; improved job system APIs | +| `UNITY_6000_0_OR_NEWER` | Unity 6 | `AssetDatabase.AssetEditingScope` (IDisposable); new GPU Resident Drawer APIs | + +There is no patch-level `_OR_NEWER` directive. Use asmdef `versionDefines` with `"name": "Unity"` if patch-level granularity is required. + +## Asmdef `versionDefines` (interval notation) + +`versionDefines` resolves one condition per entry against a named target, which can be a package name, module name, or `"Unity"`. + +```json +"versionDefines": [ + { + "name": "com.unity.formats.fbx", + "expression": "[5.0.0,6.0.0)", + "define": "LIGHTMAP_UV_TOOL_FBX_EXPORTER" + }, + { + "name": "Unity", + "expression": "2022.2", + "define": "UVTOOL_UI_TOOLKIT" + } +] +``` + +Interval syntax (NuGet-style): + +- `[1.7,2.4.1]` — inclusive on both ends. +- `[1.7,2.4.1)` — inclusive min, exclusive max. +- `2022.2` — bare value means `>= 2022.2`. + +`defineConstraints` rejects compilation unless every listed symbol is defined. `versionDefines` is for *conditional* symbols; `defineConstraints` is for *required* symbols. Use `defineConstraints` on test asmdefs that require `UNITY_INCLUDE_TESTS`. + +## Gate recipes by API + +**`PrefabUtility.EditPrefabContentsScope`** — available since 2020.1. Use the disposable form unconditionally for any package targeting 2020.1 or newer. ```csharp -#if UNITY_2022_2_OR_NEWER - // Новый API +using (var scope = new PrefabUtility.EditPrefabContentsScope(prefabAssetPath)) +{ + // scope.prefabContentsRoot is a temporary GameObject hierarchy +} +``` + +**`PrefabStage` namespace** — moved from `UnityEditor.Experimental.SceneManagement` to `UnityEditor.SceneManagement` in 2021.2. + +```csharp +#if UNITY_2021_2_OR_NEWER +using UnityEditor.SceneManagement; #else - // Фолбэк +using UnityEditor.Experimental.SceneManagement; #endif ``` -## versionDefines в asmdef -```json -"versionDefines": [{ - "name": "com.unity.formats.fbx", - "expression": "[5.0.0,6.0.0)", - "define": "HAS_FBX_EXPORTER" -}] +**`AssetDatabase.AssetEditingScope`** — Unity 6 adds an IDisposable batch scope. On earlier versions, fall back to manual `StartAssetEditing`/`StopAssetEditing` in a `try/finally`. + +```csharp +#if UNITY_6000_0_OR_NEWER +using (new AssetDatabase.AssetEditingScope()) +{ + BulkImport(); +} +#else +AssetDatabase.StartAssetEditing(); +try { BulkImport(); } +finally { AssetDatabase.StopAssetEditing(); } +#endif +``` + +**`OnPostprocessAllAssets` `didDomainReload` overload** — 2021.2+. When both overloads are defined, the richer one wins silently. + +```csharp +#if UNITY_2021_2_OR_NEWER +static void OnPostprocessAllAssets( + string[] imported, string[] deleted, string[] moved, string[] movedFrom, + bool didDomainReload) { /* ... */ } +#else +static void OnPostprocessAllAssets( + string[] imported, string[] deleted, string[] moved, string[] movedFrom) { /* ... */ } +#endif +``` + +**UI Toolkit `CreateInspectorGUI`** — usable on 2022.2+ for editor inspectors. + +```csharp +#if UNITY_2022_2_OR_NEWER +public override VisualElement CreateInspectorGUI() { /* ... */ } +#else +public override void OnInspectorGUI() { /* ... */ } +#endif ``` -| Директива | Версия | -|-----------|--------| -| UNITY_2021_3_OR_NEWER | 2021.3 LTS | -| UNITY_2022_2_OR_NEWER | 2022.2 | -| UNITY_6000_0_OR_NEWER | Unity 6 | +## Package-minimum policy + +- Declare the minimum supported Unity version in `package.json` (`"unity": "2021.3"` or similar). `unityRelease` optionally narrows to a specific release (`"0f1"`). +- Do not call any API newer than the declared minimum without an `#if` gate or a `versionDefines` entry. +- CI matrix must include the declared minimum plus every LTS in between and the current LTS. +- When bumping the minimum, use `migration-and-refactor-planner` and add a CHANGELOG entry under a major SemVer bump. + +## Fallback policy + +Every `#if` on a version directive must have an `#else` branch that compiles on the older version. Silent empty bodies are prohibited — log a warning or use the best available fallback API. Never leave an empty `#else { }`; this suppresses errors at runtime and masks missing functionality. + +## Further reading -Всегда предоставляй фолбэк в #else. +- `_shared/naming-conventions.md` +- `_shared/anti-patterns.md` From abc4e6b49b7660d1d2fd17191b76dcfe203b6bf7 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Apr 2026 07:39:19 +0000 Subject: [PATCH 03/21] docs(skills): flesh out _checklists/ with imperative action items --- .claude/skills/_checklists/batch-safety.md | 44 +++++--- .claude/skills/_checklists/package-release.md | 102 +++++++++++------- .claude/skills/_checklists/prefab-safety.md | 79 +++++++++++--- .claude/skills/_checklists/undo-safety.md | 61 ++++++++--- 4 files changed, 208 insertions(+), 78 deletions(-) diff --git a/.claude/skills/_checklists/batch-safety.md b/.claude/skills/_checklists/batch-safety.md index d8332615..8d198352 100644 --- a/.claude/skills/_checklists/batch-safety.md +++ b/.claude/skills/_checklists/batch-safety.md @@ -1,16 +1,36 @@ -# Чеклист: безопасность батч-операций +# Checklist: AssetDatabase batch safety -Проверяй если код обрабатывает множество ассетов. +Run through every item before merging code that calls `AssetDatabase.StartAssetEditing`, creates more than ten assets at once, or writes an `AssetPostprocessor`. Each bullet includes a verification command. -- [ ] `AssetDatabase.StartAssetEditing()` обёрнут в `try/finally { StopAssetEditing(); Refresh(); }` -- [ ] Progress bar для >10 ассетов: `EditorUtility.DisplayCancelableProgressBar()` -- [ ] `ClearProgressBar()` в `finally`-блоке -- [ ] Нет `AssetDatabase.Refresh()` внутри цикла — только после батча -- [ ] `AssetPostprocessor` имеет bypass-множество (`HashSet`) против рекурсии -- [ ] `GetPostprocessOrder()` указан явно (не дефолтный 0) -- [ ] Нет `AssetDatabase.ImportAsset()` внутри постпроцессора без bypass-защиты +## Pre-batch -## Эталонные реализации +- [ ] A fresh `git status` shows no unrelated working-tree drift. Verify: `git status --porcelain`. +- [ ] Every code path that opens a batch has a paired close in a `finally`. Verify: `grep -n "StartAssetEditing" path/to/file.cs` and confirm the same file contains the matching `StopAssetEditing` inside a `finally` block within the same method. +- [ ] On Unity 6 or newer, prefer `using (new AssetDatabase.AssetEditingScope())` over manual Start/Stop. Verify: `grep -n "StartAssetEditing\|AssetEditingScope" .` and confirm the disposable form is used where `#if UNITY_6000_0_OR_NEWER` applies. +- [ ] Queries against `AssetDatabase.LoadAssetAtPath`, `FindAssets`, or `GUIDToAssetPath` happen *before* opening the batch. Verify manually by reading the method top-to-bottom. -- **Батчинг с try/finally:** prefabdoctor/Editor/Core/ProjectScanActions.cs — `BatchCreateWrappers()`, `BatchRemoveMissingScripts()` -- **AssetPostprocessor с bypass:** unitymeshlab/Editor/Uv2AssetPostprocessor.cs +## Mid-batch + +- [ ] No `AssetDatabase.Refresh()` appears inside the loop. Verify: `grep -n "Refresh()" path/to/file.cs` and confirm calls are outside the loop or removed entirely. +- [ ] Progress bar (`EditorUtility.DisplayCancelableProgressBar`) is shown for any loop that iterates over more than ten assets. Verify by reading the loop body. +- [ ] The user's cancel signal (`EditorUtility.DisplayCancelableProgressBar` return value) aborts the batch cleanly, still calling `StopAssetEditing` in `finally`. + +## Post-batch + +- [ ] `StopAssetEditing` is the first statement of the `finally` block; `ClearProgressBar` is second. Verify by reading. +- [ ] A single `AssetDatabase.Refresh()` call occurs after the batch if new assets were written. Verify: `grep -c Refresh path/to/file.cs`. +- [ ] Any generated asset path is reported via `UvtLog.Info` (or the package's logger) so the user can locate it. Verify: `grep -n "Log\|Debug.Log" path/to/file.cs`. + +## AssetPostprocessor-specific + +- [ ] The postprocessor declares a static `HashSet` recursion guard populated in `try` and cleared in `finally`. Verify: `grep -n "HashSet" path/to/*Postprocessor.cs`. +- [ ] `GetPostprocessOrder()` returns an explicit integer, not the default `0`. Verify: `grep -n "GetPostprocessOrder" path/to/*Postprocessor.cs`. +- [ ] `GetVersion()` returns an integer that increments whenever the postprocessor's behavior changes. Verify by reading and comparing to the previous git SHA. +- [ ] No `AssetDatabase.ImportAsset` is called from inside the postprocessor without passing the path through the recursion guard. +- [ ] On Unity 2021.2+, the `OnPostprocessAllAssets` overload accepts `bool didDomainReload`. Verify: `grep -n "OnPostprocessAllAssets" path/to/*Postprocessor.cs`. + +## Further reading + +- `unity-assetdatabase-tools/SKILL.md` +- `_shared/anti-patterns.md` (items 7–10) +- `_shared/version-gates.md` (AssetEditingScope recipe) diff --git a/.claude/skills/_checklists/package-release.md b/.claude/skills/_checklists/package-release.md index cf13b336..b0d8fafa 100644 --- a/.claude/skills/_checklists/package-release.md +++ b/.claude/skills/_checklists/package-release.md @@ -1,37 +1,65 @@ -# Чеклист: релиз пакета - -Проверяй перед каждым релизом / git tag. - -## Метаданные -- [ ] `package.json`: version обновлён (semver — MAJOR.MINOR.PATCH) -- [ ] `package.json`: `unity` = минимальная поддерживаемая LTS -- [ ] `package.json`: `repository.url` совпадает с реальным URL репозитория -- [ ] `package.json`: нет нестандартных полей (`type`, `main`, `module`) -- [ ] `package.json`: `dependencies` актуальны (не забыты, не лишние) - -## Документация -- [ ] `CHANGELOG.md` обновлён (формат Keep a Changelog — Added/Changed/Fixed/Removed) -- [ ] `README.md` актуален (установка, использование, требования) -- [ ] `LICENSE` присутствует и совпадает с `license` в package.json - -## Код -- [ ] asmdef: `name` соответствует `Company.PackageName.Editor`, `includePlatforms` корректен -- [ ] Namespace единообразен с авторским префиксом (`Company.PackageName`) -- [ ] Нет файлов >100 КБ без обоснования (кандидаты на декомпозицию) -- [ ] Нет захардкоженных путей (`Assets/...`), URL, magic strings -- [ ] Нет `Resources.Load` в Editor-коде -- [ ] Все мутации Unity Object обёрнуты в Undo - -## Тесты -- [ ] `Tests/Editor/` существует с минимум smoke-тестами -- [ ] Тесты проходят (`Window → General → Test Runner → Run All`) - -## Сборка -- [ ] Компиляция без ошибок в минимальной версии Unity из package.json -- [ ] Компиляция без ошибок в последней LTS -- [ ] `#if UNITY_*` гейты корректны — фолбэки работают - -## Публикация -- [ ] git tag совпадает с версией в package.json (напр. `v1.0.0`) -- [ ] Установка через git URL работает -- [ ] Установка через диск работает +# Checklist: UPM package release + +Run through every item before tagging a release. Each bullet includes a verification command. + +## SemVer decision + +Choose exactly one bump based on the highest-impact change in the release: + +| Bump | Trigger | +|---|---| +| PATCH | Bug fixes, documentation, internal refactors. No public API change. | +| MINOR | New public API, new asmdef, new sample, additive versionDefines. No breaking change. | +| MAJOR | Any removed or renamed public type/member, Unity minimum bump, namespace change, asmdef name change. | + +- [ ] The SemVer bump matches the highest-impact change. Verify: `git log --oneline ..HEAD` and read every entry. +- [ ] `package.json` `version` has been updated and committed. Verify: `jq -r .version package.json`. + +## CHANGELOG + +- [ ] `CHANGELOG.md` follows Keep-a-Changelog format with sections `Added` / `Changed` / `Deprecated` / `Removed` / `Fixed` / `Security`. Verify: `head -30 CHANGELOG.md`. +- [ ] Every commit since the previous tag has a CHANGELOG entry, or is explicitly excluded as internal. Verify: diff `git log ..HEAD --oneline` against `CHANGELOG.md`. +- [ ] The new version header matches `package.json` `version` exactly. Verify: `grep -n "^## \[" CHANGELOG.md | head -1`. + +## package.json + +- [ ] `name` is `com.sasharx.` and matches the repository folder name. Verify: `jq -r .name package.json`. +- [ ] `unity` field matches the declared minimum; `unityRelease` is set if a specific patch is required. Verify: `jq -r '.unity, .unityRelease' package.json`. +- [ ] `repository.url` points to the canonical `https://github.com/SashaRX/.git` URL. Verify: `jq -r .repository.url package.json` and compare to `git remote get-url origin`. +- [ ] `dependencies` entries resolve in the latest Unity Package Manager. Verify: open in Unity and watch `Window > Package Manager` for resolve errors. +- [ ] `samples` array entries correspond one-to-one to folders under `Samples~/`. Verify: `jq -r '.samples[].path' package.json | sed 's|^Samples~/||'` matches `ls Samples~`. +- [ ] No non-standard fields (`type`, `main`, `module`) are present. Verify: `jq 'keys' package.json`. + +## Asmdef audit + +- [ ] Runtime asmdef has empty `includePlatforms` and empty `excludePlatforms`. Verify: `jq '.includePlatforms, .excludePlatforms' Runtime/*.asmdef`. +- [ ] Editor asmdef has `includePlatforms: ["Editor"]`. Verify: `jq '.includePlatforms' Editor/*.asmdef`. +- [ ] Test asmdef has `defineConstraints: ["UNITY_INCLUDE_TESTS"]`. Verify: `jq '.defineConstraints' Tests/**/*.asmdef`. +- [ ] No Runtime asmdef references an Editor asmdef. Verify: `jq '.references' Runtime/*.asmdef` and confirm none end in `.Editor`. +- [ ] `rootNamespace` of every asmdef matches the namespace used in the files under it. Verify by reading. + +## Documentation and license + +- [ ] `README.md` install-via-git-URL section points at the current default branch or tag. Verify: `grep -n "git\+https" README.md`. +- [ ] `LICENSE` exists at the package root and its SPDX identifier matches `license` in `package.json`. Verify: `jq -r .license package.json` and `head -3 LICENSE`. +- [ ] `Documentation~/` renders (index.md or TableOfContents present). Verify: `ls Documentation~` if present. + +## Build and tests + +- [ ] Compile passes on the declared minimum Unity version. Verify: CI result on the matrix minimum job. +- [ ] Compile passes on the current LTS. Verify: CI result. +- [ ] Every `#if UNITY_*` gate has an `#else` branch that compiles. Verify: `grep -rn "^#if UNITY_" Editor Runtime` and read each site. +- [ ] EditMode tests pass. Verify: CI result, or `Window > General > Test Runner > Run All` in the editor. + +## Tag and publish + +- [ ] The git tag name matches `v` exactly (e.g., `v1.2.3`). Verify: `git tag -l | tail`. +- [ ] The tag points at HEAD of the default branch after CHANGELOG + version bump commits. Verify: `git log --oneline | head -1`. +- [ ] Installation via `https://github.com/SashaRX/.git` resolves the tagged version in a consumer project's `Packages/manifest.json`. Verify in a scratch Unity project. + +## Further reading + +- `unity-package-reviewer/SKILL.md` +- `unity-package-architect/SKILL.md` +- `_shared/naming-conventions.md` +- `_shared/anti-patterns.md` (items 16–20) diff --git a/.claude/skills/_checklists/prefab-safety.md b/.claude/skills/_checklists/prefab-safety.md index 25e29697..3333a13e 100644 --- a/.claude/skills/_checklists/prefab-safety.md +++ b/.claude/skills/_checklists/prefab-safety.md @@ -1,19 +1,68 @@ -# Чеклист: Prefab-безопасность +# Checklist: Prefab safety -Проверяй если код редактирует prefab-ассеты или оверрайды. +Run through every item before merging code that edits a prefab asset, a prefab instance override, or opens a PrefabStage. Each bullet includes a verification command. -- [ ] Редактирование prefab-ассета через `PrefabUtility.EditPrefabContentsScope` -- [ ] Или через `LoadPrefabContents` / `UnloadPrefabContents` (с обязательным UnloadPrefabContents в finally) -- [ ] НЕ через `LoadAssetAtPath` → мутация → `SaveAssets` -- [ ] Temp-instance паттерн: `InstantiatePrefab → try { SaveAsPrefabAsset } finally { DestroyImmediate(instance) }` -- [ ] Оверрайды: `GetPropertyModifications/SetPropertyModifications`, НЕ прямая мутация target-полей -- [ ] `Undo.RecordObject` ПЕРЕД `SetPropertyModifications` -- [ ] `#if UNITY_2022_2_OR_NEWER` для `RemoveUnusedOverrides` и подобных новых API -- [ ] Фолбэк в `#else` для старых версий Unity +## Which context are you in? -## Эталонные реализации +| Context | API path | +|---|---| +| Prefab asset (on disk, no instance) | `PrefabUtility.EditPrefabContentsScope` (2020.1+) | +| Prefab instance in a scene (override edit) | `Undo.RecordObject` + `PrefabUtility.RecordPrefabInstancePropertyModifications` | +| Prefab open in Prefab Mode (PrefabStage) | Scene-instance rules via `PrefabStageUtility.GetCurrentPrefabStage()` | +| Creating a prefab from an instance | `PrefabUtility.SaveAsPrefabAsset` with a temp-instance pattern | -- **EditPrefabContentsScope:** prefabdoctor/Editor/Core/ProjectScanActions.cs — `RemoveMissingScripts()` -- **Temp-instance:** prefabdoctor/Editor/Core/ProjectScanActions.cs — `CreateFbxWrapper()` -- **PropertyModifications:** prefabdoctor/Editor/Core/OverrideActions.cs — `CleanOrphans()`, `RemoveModification()` -- **Version gate:** prefabdoctor/Editor/Core/ProjectScanActions.cs — `BatchRemoveUnusedOverrides()` +## Prefab asset edit (preferred: disposable scope) + +```csharp +using (var scope = new PrefabUtility.EditPrefabContentsScope(prefabAssetPath)) +{ + var root = scope.prefabContentsRoot; + Mutate(root); + // scope.Dispose saves and unloads automatically +} +``` + +- [ ] `EditPrefabContentsScope` is used for every asset-on-disk edit on Unity 2020.1+. Verify: `grep -n "EditPrefabContentsScope" path/to/file.cs`. +- [ ] If the disposable form is unavailable (pre-2020.1 fallback), the explicit form is used with both `LoadPrefabContents` and `UnloadPrefabContents(root, true)` in `try/finally`. Verify: `grep -n "LoadPrefabContents\|UnloadPrefabContents" path/to/file.cs`. +- [ ] No code path edits a prefab asset via `AssetDatabase.LoadAssetAtPath()` followed by direct mutation. Verify: `grep -n "LoadAssetAtPath" path/to/file.cs` and confirm the result is only read, never mutated. + +## Prefab instance override edit (scene) + +```csharp +Undo.RecordObject(component, "Edit override"); +component.fieldValue = newValue; +PrefabUtility.RecordPrefabInstancePropertyModifications(component); +``` + +- [ ] BOTH calls are present, in THIS order. Verify: `grep -n -B1 -A3 "RecordPrefabInstancePropertyModifications" path/to/file.cs`. +- [ ] `serializedObject.ApplyModifiedProperties()` alone is insufficient for instance overrides that bypass the inspector — the explicit `RecordPrefabInstancePropertyModifications` call is still required. + +## Temp-instance pattern (save as prefab) + +```csharp +var instance = (GameObject)PrefabUtility.InstantiatePrefab(sourcePrefab); +try +{ + Mutate(instance); + PrefabUtility.SaveAsPrefabAsset(instance, newPrefabPath); +} +finally +{ + Object.DestroyImmediate(instance); +} +``` + +- [ ] The temp instance is destroyed in `finally`, not at the end of `try`. Verify: read the method. +- [ ] The source prefab path is absolute to the project (`Assets/…` or `Packages/…`), forward-slashes only. Verify: `grep -n '\\\\' path/to/file.cs`. + +## PrefabStage + +- [ ] The namespace import is gated for 2021.2 (moved from `UnityEditor.Experimental.SceneManagement`). Verify: `grep -n "PrefabStage" path/to/file.cs` and read surrounding `#if UNITY_2021_2_OR_NEWER`. +- [ ] `PrefabStageUtility.GetCurrentPrefabStage()` nullability is checked before use. + +## Further reading + +- `unity-undo-prefab-safety/SKILL.md` +- `_shared/anti-patterns.md` (items 1–5) +- `_shared/version-gates.md` (PrefabStage + EditPrefabContentsScope gates) +- `_checklists/undo-safety.md` diff --git a/.claude/skills/_checklists/undo-safety.md b/.claude/skills/_checklists/undo-safety.md index ae356a6d..7229505f 100644 --- a/.claude/skills/_checklists/undo-safety.md +++ b/.claude/skills/_checklists/undo-safety.md @@ -1,18 +1,51 @@ -# Чеклист: Undo-безопасность +# Checklist: Undo safety -Проверяй перед каждым коммитом, если код мутирует Unity Object. +Run through every item before merging code that mutates a scene `GameObject`, a `Component`, or a serialized property of a Unity object. Each bullet includes a verification command. -- [ ] Каждая мутация Unity Object обёрнута в `Undo.RecordObject()` ДО мутации -- [ ] Группа операций: `Undo.SetCurrentGroupName() → GetCurrentGroup() → CollapseUndoOperations()` -- [ ] Или RAII: `using var scope = new UndoGroupScope(name);` -- [ ] `DestroyImmediate` только для temp-объектов в `finally`-блоке -- [ ] Для сценных объектов: `Undo.DestroyObjectImmediate()` вместо `Object.DestroyImmediate()` -- [ ] Нет `.mesh` — только `.sharedMesh` + клон перед мутацией -- [ ] CustomEditor: только через `SerializedObject.ApplyModifiedProperties()` (автоматический Undo) -- [ ] `SetPropertyModifications` предварён `Undo.RecordObject` +## Decision tree (pick one path) -## Эталонные реализации +1. **Editing a scene instance field?** → use `Undo.RecordObject(obj, name)` before the mutation; mutate via `SerializedObject` when possible. +2. **Reparenting a transform?** → use `Undo.SetTransformParent(child, newParent, name)`; do NOT set `transform.parent` directly. +3. **Adding a component?** → use `Undo.AddComponent(go)`; do NOT call `go.AddComponent()` directly. +4. **Destroying an object?** → use `Undo.DestroyObjectImmediate(obj)` in editor contexts; raw `Object.DestroyImmediate` is for temporary previews only. +5. **Creating a new object that must undo as a single unit?** → `Undo.RegisterCreatedObjectUndo(obj, name)` immediately after creation, before any further mutation. +6. **Editing a prefab instance override?** → see `prefab-safety.md`. +7. **Editing a prefab asset?** → see `prefab-safety.md`. -- **Undo-группы:** prefabdoctor/Editor/Core/OverrideActions.cs -- **UndoGroupScope RAII:** unitymeshlab/Editor/MeshHygieneUtility.cs -- **Mesh clone + Undo:** unitymeshlab/Editor/MeshHygieneUtility.cs — `PrepareWritable()` +## Checks + +- [ ] `Undo.RecordObject` is called BEFORE the mutation, not after. Verify: `grep -n -B1 -A3 "RecordObject" path/to/file.cs` and read. +- [ ] Every related mutation is collapsed into a single undo group. Verify: grep for `IncrementCurrentGroup`, `SetCurrentGroupName`, `CollapseUndoOperations` around the batch. +- [ ] No bare `go.AddComponent()` in editor code. Verify: `grep -rn --include='*.cs' "\.AddComponent<" Editor/`. +- [ ] No bare `DestroyImmediate` in editor code except for explicitly-temporary objects created and destroyed in the same `using`/`try-finally`. Verify: `grep -rn --include='*.cs' "DestroyImmediate" Editor/`. +- [ ] No direct assignment to `transform.parent`. Verify: `grep -rn --include='*.cs' "transform.parent =" Editor/`. +- [ ] `MeshFilter.mesh` is never read in editor code — only `sharedMesh`. Verify: `grep -rn --include='*.cs' "\.mesh[^F]" Editor/`. +- [ ] Every `sharedMesh` write is preceded by a clone (`Object.Instantiate(filter.sharedMesh)`) unless the intent is to mutate the asset. Verify manually. +- [ ] `CustomEditor` code mutates only through `SerializedObject.FindProperty` + `ApplyModifiedProperties`; no direct field writes on `target`. Verify: read the `OnInspectorGUI` body. + +## Undo group idiom + +```csharp +var group = Undo.GetCurrentGroup(); +Undo.SetCurrentGroupName("Refactor selection"); +try +{ + foreach (var obj in Selection.objects) + { + Undo.RecordObject(obj, "Refactor"); + Mutate(obj); + } +} +finally +{ + Undo.CollapseUndoOperations(group); +} +``` + +- [ ] The idiom above (or an equivalent `using` helper) is used wherever more than one object is mutated in a single user action. + +## Further reading + +- `unity-undo-prefab-safety/SKILL.md` +- `_shared/anti-patterns.md` (items 1–5) +- `_checklists/prefab-safety.md` for prefab instance/asset paths From 916f9a51a8ece46cc56e7eb2858704c97c7887e9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Apr 2026 07:43:13 +0000 Subject: [PATCH 04/21] docs(skills): rewrite editor + assetdb + undo + serialization skills in English --- .../skills/unity-assetdatabase-tools/SKILL.md | 185 ++++++++++++++++- .claude/skills/unity-editor-tooling/SKILL.md | 178 ++++++++++++++++- .../skills/unity-serialized-workflow/SKILL.md | 186 ++++++++++++++++- .../skills/unity-undo-prefab-safety/SKILL.md | 187 +++++++++++++++++- 4 files changed, 724 insertions(+), 12 deletions(-) diff --git a/.claude/skills/unity-assetdatabase-tools/SKILL.md b/.claude/skills/unity-assetdatabase-tools/SKILL.md index 1e724a63..a3c49bf4 100644 --- a/.claude/skills/unity-assetdatabase-tools/SKILL.md +++ b/.claude/skills/unity-assetdatabase-tools/SKILL.md @@ -1,8 +1,187 @@ --- name: unity-assetdatabase-tools -description: Используй при написании AssetPostprocessor, батчинге AssetDatabase-операций с StartAssetEditing, управлении импортом или генерации HideAndDontSave-ассетов. +description: Safely batch AssetDatabase operations, move and rename assets preserving GUIDs, and write AssetPostprocessors with recursion guards. Use when code touches AssetDatabase, AssetImporter, AssetPostprocessor, .meta files, or performs bulk asset creation, import, move, copy, or delete. Always wrap batches in try/finally with StartAssetEditing/StopAssetEditing, or AssetEditingScope on Unity 6+. +paths: ["**/*AssetPostprocessor*.cs", "**/*Importer*.cs"] --- -# AssetDatabase Tools +# unity-assetdatabase-tools -Батчинг: StartAssetEditing в try/finally. AssetPostprocessor: GetPostprocessOrder явно + bypass HashSet. Progress bar: DisplayCancelableProgressBar + ClearProgressBar в finally. ЗАПРЕЩЕНО: StartAssetEditing без try/finally, Refresh внутри цикла, Resources.Load в Editor, FindAssets без t: фильтра. +Guidance for the Unity asset pipeline: batching bulk operations, moving assets while preserving GUIDs, and writing well-behaved `AssetPostprocessor` subclasses. Asset-pipeline concerns are orthogonal to mutation concerns — scene and prefab edits belong to `unity-undo-prefab-safety`. + +## Scope and delegations + +Covered here: + +- `AssetDatabase.StartAssetEditing` / `StopAssetEditing` batching. +- `AssetDatabase.MoveAsset` / `RenameAsset` / `CopyAsset` / `DeleteAsset` semantics. +- `AssetImporter.userData` and per-importer configuration. +- `AssetPostprocessor` authoring — recursion guards, ordering, versioning. +- `OnPostprocessAllAssets` including the 2021.2+ `didDomainReload` overload. + +Delegated elsewhere: + +- **Scene or component mutation** → `unity-undo-prefab-safety`. +- **SerializedObject in a CustomEditor** → `unity-serialized-workflow`. +- **Package structure, asmdef placement** → `unity-package-architect`. + +## The batching contract + +`AssetDatabase.StartAssetEditing` is reference-counted. Every call must be paired with exactly one `StopAssetEditing` in a `finally` block. Unreleased counters leave the editor unresponsive — the import pipeline cannot drain while the counter is above zero. Queries between `Start` and `Stop` see pre-batch state; imports are deferred until `Stop`. + +```csharp +AssetDatabase.StartAssetEditing(); +try +{ + foreach (var path in paths) + CreateOneAsset(path); +} +finally +{ + AssetDatabase.StopAssetEditing(); + EditorUtility.ClearProgressBar(); +} +``` + +On Unity 6 or newer, prefer the disposable scope: + +```csharp +#if UNITY_6000_0_OR_NEWER +using (new AssetDatabase.AssetEditingScope()) +{ + foreach (var path in paths) + CreateOneAsset(path); +} +#else +AssetDatabase.StartAssetEditing(); +try { foreach (var path in paths) CreateOneAsset(path); } +finally { AssetDatabase.StopAssetEditing(); } +#endif +``` + +Do not call `AssetDatabase.LoadAssetAtPath`, `AssetDatabase.FindAssets`, or `AssetDatabase.GUIDToAssetPath` expecting up-to-date results while a batch is open — imports are deferred and queries see the pre-batch state. Gather inputs before the batch; persist outputs after it. + +Do not call `StartAssetEditing` from `EditorApplication.update` without a same-tick `StopAssetEditing`; the unbalanced counter blocks the editor update cycle. + +## Move, rename, copy, delete + +Always use AssetDatabase APIs. Filesystem operations orphan `.meta` files and break GUID links. + +| Operation | API | +|---|---| +| Move or rename | `AssetDatabase.MoveAsset(from, to)` returns `""` on success, error string otherwise | +| Rename in place | `AssetDatabase.RenameAsset(path, newName)` — cannot change file extension | +| Copy | `AssetDatabase.CopyAsset(from, to)` | +| Delete | `AssetDatabase.DeleteAsset(path)` — returns `bool`; also deletes `.meta` | + +Always check `MoveAsset` and `CopyAsset` results. Never use `File.Move`, `File.Copy`, `File.Delete`, or `Directory.Delete` on anything under `Assets/` or `Packages//`. + +Paths are relative to the project root, use forward slashes only, and must include the file extension. + +## AssetPostprocessor authoring + +A postprocessor is a class extending `AssetPostprocessor`. It is discovered automatically; Unity calls the declared `OnPreprocess*` / `OnPostprocess*` methods at the appropriate pipeline stage. + +```csharp +internal sealed class Uv2AssetPostprocessor : AssetPostprocessor +{ + private static readonly HashSet s_Guard = new HashSet(); + + public override int GetPostprocessOrder() => 1000; + public override uint GetVersion() => 3; // increment on behavior change + + private void OnPreprocessModel() + { + if (!s_Guard.Add(assetPath)) return; + try + { + var importer = (ModelImporter)assetImporter; + ApplyRepoConventions(importer); + } + finally + { + s_Guard.Remove(assetPath); + } + } + +#if UNITY_2021_2_OR_NEWER + static void OnPostprocessAllAssets( + string[] imported, string[] deleted, string[] moved, string[] movedFromAssetPaths, + bool didDomainReload) + { + if (didDomainReload) RebuildCaches(); + } +#else + static void OnPostprocessAllAssets( + string[] imported, string[] deleted, string[] moved, string[] movedFromAssetPaths) + { + RebuildCaches(); + } +#endif +} +``` + +Rules: + +- **`GetPostprocessOrder`** returns an explicit non-zero integer whenever order matters relative to other postprocessors; default `0` is ambiguous. +- **`GetVersion`** returns a monotonically-increasing integer; increment whenever the postprocessor's behavior changes so the import cache is invalidated. +- **Recursion guard** is a static `HashSet` keyed on `assetPath`, populated in `try`, cleared in `finally`. Required for any postprocessor that calls `AssetDatabase.ImportAsset` or re-triggers import. +- **`OnPostprocessAllAssets`** is static (not instance) and does NOT participate in `GetPostprocessOrder`. For ordering across postprocessors at this stage, use assembly dependencies (asmdef references). +- **Ship postprocessors as DLLs in production**. A postprocessor with a compile error locks the asset pipeline — no asset imports, including the fix for the error. DLLs bypass the compilation cycle. + +## Importer configuration + +- `AssetImporter.userData` is a per-asset, JSON-friendly string blob. Use it for round-trip state that belongs to the importer, not to the asset. Example: the last successful import timestamp. +- `importer.SaveAndReimport()` flushes importer changes and re-triggers import. Prefer this over `AssetDatabase.ImportAsset` when the change came from user configuration. + +## Good vs bad pattern pairs + +**Bad: importing 10 000 textures without a batch** + +```csharp +foreach (var guid in guids) +{ + var path = AssetDatabase.GUIDToAssetPath(guid); + AssetDatabase.ImportAsset(path); // each call triggers a full refresh pass +} +AssetDatabase.Refresh(); +``` + +Time complexity is O(N²) on the asset pipeline. + +**Good: batched** + +```csharp +AssetDatabase.StartAssetEditing(); +try +{ + foreach (var guid in guids) + { + var path = AssetDatabase.GUIDToAssetPath(guid); + AssetDatabase.ImportAsset(path); + } +} +finally +{ + AssetDatabase.StopAssetEditing(); +} +``` + +**Bad: `File.Move` then `AssetDatabase.Refresh`** + +```csharp +File.Move(oldPath, newPath); +AssetDatabase.Refresh(); // .meta is orphaned; GUID link broken +``` + +**Good: `AssetDatabase.MoveAsset`** + +```csharp +var err = AssetDatabase.MoveAsset(oldPath, newPath); +if (!string.IsNullOrEmpty(err)) throw new IOException(err); +``` + +## Further reading + +- `_checklists/batch-safety.md` +- `_shared/version-gates.md` +- `_shared/anti-patterns.md` diff --git a/.claude/skills/unity-editor-tooling/SKILL.md b/.claude/skills/unity-editor-tooling/SKILL.md index 9b78fa77..9185360d 100644 --- a/.claude/skills/unity-editor-tooling/SKILL.md +++ b/.claude/skills/unity-editor-tooling/SKILL.md @@ -1,8 +1,180 @@ --- name: unity-editor-tooling -description: Используй при создании EditorWindow, MenuItem, [InitializeOnLoad], [OnOpenAsset], AssetModificationProcessor или других точек входа Unity Editor. +description: Write and review Unity Editor-only code — EditorWindow, CustomEditor, PropertyDrawer, menu items, IMGUI and UI Toolkit inspectors. Use when creating files under an Editor/ folder, writing [MenuItem], [CustomEditor], [CustomPropertyDrawer], [InitializeOnLoad], or any code inside an asmdef with includePlatforms Editor. Not for AssetDatabase batching or prefab mutation — delegate to unity-assetdatabase-tools and unity-undo-prefab-safety. +paths: ["**/Editor/**/*.cs"] --- -# Unity Editor Tooling +# unity-editor-tooling -Точки входа: EditorWindow (GetWindow/CreateInstance), AssetModificationProcessor (OnWillSaveAssets, IsOpenForEdit), [OnOpenAsset], [InitializeOnLoad]. Запреты: ручной JSON, UnityWebRequest без timeout, статическое состояние без контракта, тяжёлые операции в OnGUI, Editor API в Runtime asmdef. +Authoring and review guidance for Unity Editor-only code: EditorWindow, CustomEditor, PropertyDrawer, menu items, and initialization hooks. This skill is the umbrella entry point for editor surfaces; it delegates to siblings for asset-pipeline and mutation concerns. + +## Scope and delegations + +This skill covers: + +- EditorWindow authoring (IMGUI and UI Toolkit). +- CustomEditor scaffolding (the serialized-property logic belongs to `unity-serialized-workflow`). +- PropertyDrawer and DecoratorDrawer. +- `[MenuItem]`, `[InitializeOnLoad]`, `[InitializeOnLoadMethod]`, `[OnOpenAsset]`, `AssetModificationProcessor`. + +Delegated elsewhere: + +- **AssetDatabase batching, import pipeline, `AssetPostprocessor`** → `unity-assetdatabase-tools`. +- **Any mutation of scene objects, components, prefabs, or prefab overrides** → `unity-undo-prefab-safety`. +- **`SerializedObject` / `SerializedProperty` lifecycle inside CustomEditor/PropertyDrawer** → `unity-serialized-workflow`. +- **asmdef and package layout** → `unity-package-architect`. + +## Editor asmdef shape + +Every editor asmdef sits under an `Editor/` folder and declares an Editor-only platform filter. + +```json +{ + "name": "SashaRX..Editor", + "rootNamespace": "SashaRX..Editor", + "references": [ "SashaRX." ], + "includePlatforms": [ "Editor" ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "autoReferenced": false, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} +``` + +Rules: + +- `includePlatforms: ["Editor"]` and `excludePlatforms: []` — one is non-empty, the other empty. +- `autoReferenced: false` to keep the editor asmdef out of the default reference set of consumer assemblies. +- `name` equals the asmdef file basename, e.g., `SashaRX.UnityMeshLab.Editor.asmdef`. + +## EditorWindow lifecycle + +IMGUI and UI Toolkit entry points live side by side; choose one per window based on Unity version and preferred style. + +```csharp +public class HealthWindow : EditorWindow +{ + [MenuItem("Tools/Prefab Doctor/Open Health Window")] + public static void Open() => GetWindow("Prefab Health"); + + private void OnEnable() { /* acquire resources, subscribe */ } + private void OnDisable() { /* release resources, unsubscribe */ } + private void OnInspectorUpdate() { Repaint(); } + +#if UNITY_2022_2_OR_NEWER + private void CreateGUI() { /* UI Toolkit root; ignore OnGUI if used */ } +#endif + + private void OnGUI() { /* IMGUI fallback */ } +} +``` + +- Acquire subscriptions in `OnEnable`, release in `OnDisable`. `EditorApplication.update`, `Undo.undoRedoPerformed`, `Selection.selectionChanged` are the common targets. +- Call `Repaint` from `OnInspectorUpdate` (runs 10× per second) instead of `EditorApplication.update`; this keeps UI lag bounded without burning frames. +- Use `CreateGUI` for UI Toolkit on 2022.2+. When both are defined, IMGUI `OnGUI` is ignored unless the visual element root is empty. + +## CustomEditor skeleton + +```csharp +[CustomEditor(typeof(PrefabHealthProfile))] +public class PrefabHealthProfileEditor : Editor +{ + private SerializedProperty _threshold; + + private void OnEnable() + { + _threshold = serializedObject.FindProperty("_threshold"); + } + + public override void OnInspectorGUI() + { + serializedObject.Update(); + EditorGUILayout.PropertyField(_threshold); + serializedObject.ApplyModifiedProperties(); + } +} +``` + +The inspector body must start with `Update()` and end with `ApplyModifiedProperties()`. Everything in between is GUI. For the property-manipulation details, defer to `unity-serialized-workflow`. + +## PropertyDrawer vs DecoratorDrawer + +- `PropertyDrawer` draws a single serialized property. Called once per instance of the decorated type. +- `DecoratorDrawer` draws without a target property — useful for section headers and separators inside an inspector. Called once per attribute occurrence. + +Override `GetPropertyHeight` whenever the drawer changes vertical footprint; otherwise Unity clips subsequent rows. + +## `[MenuItem]` conventions + +- Menu root: `Tools//`; e.g., `Tools/Prefab Doctor/Open Health Window`. +- Shortcut syntax: `%` Ctrl/Cmd, `#` Shift, `&` Alt; e.g., `"Tools/Prefab Doctor/Refresh %#r"`. +- Priority integer controls grouping; keep related items within 10 of each other. +- Validation function: a sibling method with the same menu path, `validate: true`, returning `bool`: + +```csharp +[MenuItem("Tools/Prefab Doctor/Refresh", validate = true)] +private static bool RefreshValidate() => Selection.activeGameObject != null; +``` + +## `[InitializeOnLoad]` and `[InitializeOnLoadMethod]` + +- `[InitializeOnLoad]` on a class runs its static constructor on domain reload. Use for long-lived subscriptions. +- `[InitializeOnLoadMethod]` on a static method runs it on domain reload. Cheaper; prefer this when class-level state is not needed. +- Never perform blocking I/O from either hook — the editor stalls on domain reload. +- Log a single one-line message at INFO level so the user can confirm the hook fired. + +## Good vs bad pattern pairs + +**Bad: heavy work in `OnGUI`** + +```csharp +private void OnGUI() +{ + _results = AssetDatabase.FindAssets("t:Prefab"); // runs every repaint + foreach (var guid in _results) { /* … */ } +} +``` + +**Good: precompute on event, redraw on state** + +```csharp +private string[] _results = Array.Empty(); + +private void OnEnable() { Selection.selectionChanged += Refresh; Refresh(); } +private void OnDisable() { Selection.selectionChanged -= Refresh; } +private void Refresh() { _results = AssetDatabase.FindAssets("t:Prefab"); Repaint(); } + +private void OnGUI() { foreach (var guid in _results) { /* … */ } } +``` + +**Bad: direct `Selection.activeObject` mutation inside `OnGUI`** + +```csharp +private void OnGUI() { Selection.activeObject = _target; /* re-entrant paint */ } +``` + +**Good: gate on explicit user input** + +```csharp +private void OnGUI() +{ + if (GUILayout.Button("Select")) Selection.activeObject = _target; +} +``` + +## Version gates + +- Target the package's minimum Unity version declared in `package.json`. +- On 2021.3 LTS: IMGUI-first inspectors; UI Toolkit supported but limited. +- On 2022.2+: UI Toolkit inspectors (`CreateInspectorGUI`) are mature; prefer them for new code. +- Gate UI-Toolkit-only code with `#if UNITY_2022_2_OR_NEWER` and fall back to IMGUI in `#else`. + +See `_shared/version-gates.md` for the canonical recipes. + +## Further reading + +- `_shared/naming-conventions.md` +- `_shared/version-gates.md` +- `_shared/anti-patterns.md` diff --git a/.claude/skills/unity-serialized-workflow/SKILL.md b/.claude/skills/unity-serialized-workflow/SKILL.md index 214c06bd..90d68e6d 100644 --- a/.claude/skills/unity-serialized-workflow/SKILL.md +++ b/.claude/skills/unity-serialized-workflow/SKILL.md @@ -1,8 +1,188 @@ --- name: unity-serialized-workflow -description: Используй при работе с SerializedObject/SerializedProperty, создании CustomEditor/PropertyDrawer или сравнении сериализованных значений. +description: Implement CustomEditor and PropertyDrawer classes with correct SerializedObject lifecycle. Use when writing CustomEditor, CustomPropertyDrawer, EditorWindow with Inspector-style panels, or any code using SerializedObject/SerializedProperty/FindProperty/FindPropertyRelative. Always call serializedObject.Update() first and ApplyModifiedProperties() last; never call Update() mid-modification. +paths: ["**/Editor/**/*.cs"] --- -# Unity Serialized Workflow +# unity-serialized-workflow -Цикл: Update() → FindProperty → ApplyModifiedProperties(). ЗАПРЕЩЕНО: мутация target cast. BeginProperty/EndProperty в PropertyDrawer обязательны. Read-only доступ через new SerializedObject(obj).FindProperty(path). +Canonical rules for editing Unity objects through the `SerializedObject` / `SerializedProperty` API. This skill is scoped to the lifecycle inside a `CustomEditor`, `PropertyDrawer`, or editor window inspector panel. Mutation outside that lifecycle belongs to `unity-undo-prefab-safety`. + +## Scope and delegations + +Covered here: + +- `OnInspectorGUI` lifecycle: `Update` → `Find*` → GUI → `ApplyModifiedProperties`. +- `FindProperty` vs `FindPropertyRelative` vs `GetArrayElementAtIndex`. +- Multi-object editing. +- `EditorGUI.BeginChangeCheck` / `EndChangeCheck` and when it is the right hammer. +- `PropertyDrawer.OnGUI` with `BeginProperty` / `EndProperty`. +- `ISerializationCallbackReceiver`, `[SerializeReference]`, `OnValidate`. + +Delegated elsewhere: + +- **EditorWindow, CustomEditor skeleton, menu items** → `unity-editor-tooling`. +- **Asset batching and postprocessors** → `unity-assetdatabase-tools`. +- **Scene or prefab mutation outside the inspector** → `unity-undo-prefab-safety`. + +## Canonical `OnInspectorGUI` skeleton + +```csharp +[CustomEditor(typeof(Foo))] +public class FooEditor : Editor +{ + private SerializedProperty _name; + private SerializedProperty _items; + + private void OnEnable() + { + _name = serializedObject.FindProperty("_name"); + _items = serializedObject.FindProperty("_items"); + } + + public override void OnInspectorGUI() + { + serializedObject.Update(); + + EditorGUILayout.PropertyField(_name); + EditorGUILayout.PropertyField(_items, includeChildren: true); + + serializedObject.ApplyModifiedProperties(); + } +} +``` + +The body starts with `Update()` and ends with `ApplyModifiedProperties()`. Everything between the two is GUI. Calling `Update()` mid-body overwrites unapplied edits; calling `ApplyModifiedProperties()` mid-body commits partial state. + +Cache `SerializedProperty` handles in `OnEnable`; creating them every frame inside `OnInspectorGUI` is measurably slower and risks drift when the target changes. + +## `FindProperty` vs `FindPropertyRelative` + +- `serializedObject.FindProperty("_fieldName")` — absolute from the object root. +- `parentProperty.FindPropertyRelative("childField")` — relative to a property handle; used for nested structs and classes. +- `parentProperty.GetArrayElementAtIndex(i)` — element of an array or list property. + +Serialized field names match the backing C# field names (including leading `_` or `m_`). When a field is renamed, add `[FormerlySerializedAs("oldName")]` to preserve asset compatibility. + +## Multi-object editing + +```csharp +var so = new SerializedObject(targets); // plural: all selected targets +so.Update(); +// edits apply to every target via ApplyModifiedProperties +so.ApplyModifiedProperties(); +``` + +The `targets` array is populated when the user multi-selects objects with the same type. Property values that differ across the selection appear with a mixed-value indicator; reading `so.isEditingMultipleObjects` confirms the state. + +## Property iteration + +```csharp +var prop = serializedObject.GetIterator(); +var enterChildren = true; +while (prop.NextVisible(enterChildren)) +{ + enterChildren = false; + EditorGUILayout.PropertyField(prop, includeChildren: true); +} +``` + +Important rules: + +- Copy the iterator with `.Copy()` before recursing into children if you need a stable cursor. +- `NextVisible(true)` enters visible children once; subsequent calls should pass `false` to stay at the same nesting level. + +## `BeginChangeCheck` / `EndChangeCheck` + +Use the change-check block when you need to run logic ONLY if the user modified a value in the GUI pass. + +```csharp +EditorGUI.BeginChangeCheck(); +EditorGUILayout.PropertyField(_name); +if (EditorGUI.EndChangeCheck()) +{ + OnNameChanged(); // custom side-effect +} +serializedObject.ApplyModifiedProperties(); // still required +``` + +`EndChangeCheck` detects that the GUI pass saw an interaction — it does NOT replace `ApplyModifiedProperties`. Always call both. + +`ApplyModifiedPropertiesWithoutUndo()` exists for edits that should not appear in the undo stack (e.g., internal cache updates). Use it sparingly; the default `ApplyModifiedProperties` records undo automatically. + +## `PropertyDrawer.OnGUI` + +```csharp +[CustomPropertyDrawer(typeof(MyAttribute))] +public class MyDrawer : PropertyDrawer +{ + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + label = EditorGUI.BeginProperty(position, label, property); + try + { + EditorGUI.PropertyField(position, property, label, includeChildren: true); + } + finally + { + EditorGUI.EndProperty(); + } + } + + public override float GetPropertyHeight(SerializedProperty property, GUIContent label) + => EditorGUI.GetPropertyHeight(property, label, includeChildren: true); +} +``` + +- `BeginProperty` / `EndProperty` wrap every drawer so prefab-override highlighting works. Missing them breaks inspector chrome silently. +- Override `GetPropertyHeight` whenever the drawer renders more than a single row. + +## `[SerializeField]` vs `[SerializeReference]` vs `ISerializationCallbackReceiver` + +- `[SerializeField]` — default; serializes a concrete type by value. Fields must be public or have this attribute to appear in the inspector. +- `[SerializeReference]` — serializes a polymorphic reference; required for interface fields and abstract base classes. The referenced type must be `[Serializable]`. +- `ISerializationCallbackReceiver` — implement to customize the pre-serialize / post-deserialize step (e.g., round-trip a `Dictionary` as two parallel lists). + +## `OnValidate` + +`OnValidate()` runs on script compile and on inspector edit. Use it for invariants that must hold whenever serialized data changes (clamping, ensuring non-null fallbacks). It runs on the `MonoBehaviour` / `ScriptableObject` instance after deserialization; setter-based invariants that bypass the serializer belong here. + +## Good vs bad pattern pairs + +**Bad: direct field mutation inside a CustomEditor** + +```csharp +public override void OnInspectorGUI() +{ + var foo = (Foo)target; + foo.value = EditorGUILayout.IntField("Value", foo.value); // bypasses undo + prefab overrides +} +``` + +**Good:** + +```csharp +public override void OnInspectorGUI() +{ + serializedObject.Update(); + EditorGUILayout.PropertyField(serializedObject.FindProperty("_value")); + serializedObject.ApplyModifiedProperties(); +} +``` + +**Bad: missing `EndProperty`** + +```csharp +public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) +{ + EditorGUI.PropertyField(position, property); // prefab override styling broken +} +``` + +**Good:** see the `PropertyDrawer.OnGUI` template above. + +## Further reading + +- `_shared/anti-patterns.md` (items 11–13) +- `_shared/version-gates.md` +- `unity-editor-tooling/SKILL.md` diff --git a/.claude/skills/unity-undo-prefab-safety/SKILL.md b/.claude/skills/unity-undo-prefab-safety/SKILL.md index e1767980..74beb25b 100644 --- a/.claude/skills/unity-undo-prefab-safety/SKILL.md +++ b/.claude/skills/unity-undo-prefab-safety/SKILL.md @@ -1,8 +1,189 @@ --- name: unity-undo-prefab-safety -description: Используй при мутации prefab-ассетов, записи Undo-групп, редактировании prefab-оверрайдов или клонировании asset-backed мешей перед модификацией. +description: Make every editor mutation undoable and every prefab edit safe. Use when code modifies scene GameObjects, components, prefab assets, or prefab instance overrides, or when using Undo, PrefabUtility, PrefabStage, or EditorUtility.SetDirty. ALWAYS call Undo.RecordObject before mutation, PrefabUtility.RecordPrefabInstancePropertyModifications after instance edits, and EditPrefabContentsScope (2020.1+) for asset edits. Do not use File.* for assets — use AssetDatabase APIs (delegate to unity-assetdatabase-tools). +paths: ["**/Editor/**/*.cs"] --- -# Undo и Prefab Safety +# unity-undo-prefab-safety -Undo-группы: SetCurrentGroupName → GetCurrentGroup → CollapseUndoOperations. RAII: UndoGroupScope. Prefab: EditPrefabContentsScope. Temp-instance: InstantiatePrefab → try {SaveAsPrefabAsset} finally {DestroyImmediate}. Оверрайды: Get/SetPropertyModifications. Mesh: только .sharedMesh + клон. ЗАПРЕЩЕНО: .mesh, LoadAssetAtPath→мутация→SaveAssets, DestroyImmediate без Undo. +Canonical rules for making every editor mutation undoable and every prefab edit safe. This is the flagship editor-mutation-safety skill. Asset-pipeline concerns (move, rename, batch import) live in `unity-assetdatabase-tools`. + +## Three mutation contexts + +Every editor mutation falls into exactly one context. Pick the right API for the context. + +| Context | API path | Records undo? | +|---|---|---| +| Scene instance (GameObject/Component in an open scene) | `Undo.RecordObject` + mutation; or `SerializedObject` + `ApplyModifiedProperties` | Yes | +| Prefab instance override (scene instance of a prefab, editing an override) | `Undo.RecordObject` → mutate → `PrefabUtility.RecordPrefabInstancePropertyModifications` | Yes | +| Prefab asset (on disk, no scene instance) | `PrefabUtility.EditPrefabContentsScope` (2020.1+) | Editor re-opens dirty scene; no undo — users revert via git | + +## `Undo.RecordObject`: what it covers and what it doesn't + +`Undo.RecordObject(obj, name)` captures a serialized-property-delta snapshot of `obj`. It does NOT capture: + +- Reparenting (`transform.parent = …`) — use `Undo.SetTransformParent`. +- Component addition (`gameObject.AddComponent()`) — use `Undo.AddComponent`. +- Object destruction (`Object.DestroyImmediate`) — use `Undo.DestroyObjectImmediate`. +- Object creation that must participate in the current undo group — use `Undo.RegisterCreatedObjectUndo` immediately after creation. + +For full hierarchy edits (reorder, reparent, add/remove multiple children), use `Undo.RegisterFullObjectHierarchyUndo(root, name)`. + +## Ordering rules + +- `Undo.RecordObject` MUST be called BEFORE the mutation. After-the-fact calls capture the already-mutated state. +- `Undo.RegisterCreatedObjectUndo` MUST be called AFTER creation but BEFORE any subsequent `RecordObject`. Inserting it later orphans intermediate edits. +- For prefab instance overrides: `Undo.RecordObject` first, then mutate, then `PrefabUtility.RecordPrefabInstancePropertyModifications`. All three steps, in that order, for every instance edit. + +## Grouping multiple mutations + +When a single user action causes multiple mutations, collapse them into one undo group so that one Ctrl+Z reverses the whole batch. + +```csharp +Undo.IncrementCurrentGroup(); +var group = Undo.GetCurrentGroup(); +Undo.SetCurrentGroupName("Refactor selection"); +try +{ + foreach (var obj in Selection.gameObjects) + { + Undo.RecordObject(obj.transform, "Refactor"); + obj.transform.localScale *= 2f; + } +} +finally +{ + Undo.CollapseUndoOperations(group); +} +``` + +The plan's `_checklists/undo-safety.md` repeats the idiom with a verification command. + +## Prefab asset edit (preferred: disposable scope) + +Available since Unity 2020.1. Opens the prefab in a temporary hierarchy, runs the body, saves on `Dispose`. + +```csharp +using (var scope = new PrefabUtility.EditPrefabContentsScope(prefabAssetPath)) +{ + var root = scope.prefabContentsRoot; + Mutate(root); +} +``` + +This is the correct path for every asset-on-disk edit. It avoids the `LoadPrefabContents`/`UnloadPrefabContents` ceremony and eliminates the common mistake of forgetting `UnloadPrefabContents(root, saveChanges: true)`. + +Manual pattern (pre-2020.1 fallback): + +```csharp +var root = PrefabUtility.LoadPrefabContents(prefabAssetPath); +try +{ + Mutate(root); + PrefabUtility.SaveAsPrefabAsset(root, prefabAssetPath); +} +finally +{ + PrefabUtility.UnloadPrefabContents(root); +} +``` + +## Prefab instance override edit + +```csharp +Undo.RecordObject(component, "Edit override"); +component.fieldValue = newValue; +PrefabUtility.RecordPrefabInstancePropertyModifications(component); +``` + +Both calls, in this order, for every instance edit. The `RecordObject` call alone marks the scene dirty; without `RecordPrefabInstancePropertyModifications`, the override is lost when the scene is reloaded or the prefab is re-applied. + +For reverting overrides, use `PrefabUtility.RevertObjectOverride(obj, InteractionMode.UserAction)` so the action appears in the undo history. + +## PrefabStage + +`PrefabStage` is the API for prefabs currently open in Prefab Mode. + +```csharp +#if UNITY_2021_2_OR_NEWER +using UnityEditor.SceneManagement; +#else +using UnityEditor.Experimental.SceneManagement; +#endif + +var stage = PrefabStageUtility.GetCurrentPrefabStage(); +if (stage != null) +{ + // stage.prefabContentsRoot is the live temporary hierarchy; + // edits here follow scene-instance rules, not asset rules. +} +``` + +Always null-check `GetCurrentPrefabStage()`; Prefab Mode may not be active. + +## Mesh safety + +- `MeshFilter.mesh` in edit mode clones the shared asset and leaks the clone. Always use `sharedMesh`. +- Editing `sharedMesh` mutates the authored asset for every instance; `Undo.RecordObject` alone does NOT make this safe for subsequent project state. Clone first: `var clone = Object.Instantiate(filter.sharedMesh);`. +- `Mesh.UploadMeshData(true)` frees CPU-side mesh data after upload; the mesh becomes non-readable. Only call this when the mesh will not be inspected or re-edited. + +## `EditorUtility.SetDirty` + +`EditorUtility.SetDirty(asset)` marks an asset dirty for serialization but does NOT record an undo entry. It is a companion to `Undo.RecordObject`, not a substitute. Call order: `Undo.RecordObject(asset, name)` → mutate → `EditorUtility.SetDirty(asset)` if the asset is a `ScriptableObject` or similar. + +## `EditorSceneManager.MarkSceneDirty` + +`Undo.RecordObject` marks the scene dirty automatically. Call `MarkSceneDirty` explicitly only when mutating scene-level state that `RecordObject` does not cover (loaded scene metadata, lightmap data). + +## Good vs bad pattern pairs + +**Bad: `AddComponent` without undo** + +```csharp +var c = go.AddComponent(); +c.value = 42; +``` + +**Good:** + +```csharp +var c = Undo.AddComponent(go); +Undo.RecordObject(c, "Configure"); +c.value = 42; +``` + +**Bad: reparenting via `transform.parent`** + +```csharp +child.transform.parent = newParent.transform; +``` + +**Good:** + +```csharp +Undo.SetTransformParent(child.transform, newParent.transform, "Reparent"); +``` + +**Bad: prefab asset edit through `LoadAssetAtPath`** + +```csharp +var go = AssetDatabase.LoadAssetAtPath(path); +go.GetComponent().value = 1; // mutates asset directly; no serialization guarantee +AssetDatabase.SaveAssets(); +``` + +**Good: scope** + +```csharp +using (var scope = new PrefabUtility.EditPrefabContentsScope(path)) +{ + scope.prefabContentsRoot.GetComponent().value = 1; +} +``` + +## Further reading + +- `_checklists/undo-safety.md` +- `_checklists/prefab-safety.md` +- `_shared/anti-patterns.md` +- `_shared/version-gates.md` From 6ef5348877594e78c7b0393345b149caa170e6e6 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Apr 2026 07:44:55 +0000 Subject: [PATCH 05/21] docs(skills): rewrite UPM architect + reviewer skills in English --- .../skills/unity-package-architect/SKILL.md | 224 +++++++++++++++++- .../skills/unity-package-reviewer/SKILL.md | 168 ++++++++++++- 2 files changed, 386 insertions(+), 6 deletions(-) diff --git a/.claude/skills/unity-package-architect/SKILL.md b/.claude/skills/unity-package-architect/SKILL.md index 35e5e300..55361834 100644 --- a/.claude/skills/unity-package-architect/SKILL.md +++ b/.claude/skills/unity-package-architect/SKILL.md @@ -1,8 +1,226 @@ --- name: unity-package-architect -description: Используй при проектировании или исправлении package.json, asmdef, структуры Editor/Runtime/Tests~/Samples~/Documentation~/Native~, или ограничений версии Unity. +description: Author Unity UPM package skeletons with canonical layout — package.json, asmdef boundaries, Runtime/Editor/Tests split, Samples~ and Documentation~ tilde-hidden folders. Use when creating a new UPM package, restructuring a package, editing package.json, or adding/splitting asmdefs. Namespaces follow SashaRX.; see _shared/naming-conventions.md. +paths: ["**/package.json", "**/*.asmdef", "**/*.asmref"] --- -# Unity Package Architect +# unity-package-architect -Эталон: package.json (com.company.name, semver, LTS unity, нет нестандартных полей), asmdef (Company.Pkg.Editor, includePlatforms:["Editor"]), Editor/Runtime/Tests~/Documentation~/Samples~/Native~. Нейминг: Company.PackageName. Для монорепо: git URL + ?path=subfolder. +Authoritative skill for Unity UPM package layout. Every new or restructured package must conform to this shape. The companion `unity-package-reviewer` audits against these rules; `unity-package-bootstrap` scaffolds a fresh package from the template. + +## Scope and delegations + +Covered here: + +- Folder tree: `Editor/`, `Runtime/`, `Tests/`, `Samples~/`, `Documentation~/`, `Native~/`. +- `package.json` field rules. +- Asmdef pair pattern (Runtime + Editor + Tests). +- `versionDefines` and `defineConstraints`. +- `Samples~` and `samples` array correspondence. +- `.gitignore` / `.npmignore` rules for UPM. + +Delegated elsewhere: + +- **Pre-release checks** → `unity-package-reviewer` and `_checklists/package-release.md`. +- **Migration (namespace rename, Unity bump)** → `migration-and-refactor-planner`. +- **New package from template** → `unity-package-bootstrap`. + +## Canonical folder tree + +``` +/ +├── package.json +├── README.md +├── CHANGELOG.md +├── LICENSE +├── Editor/ +│ ├── SashaRX..Editor.asmdef +│ └── /*.cs +├── Runtime/ +│ ├── SashaRX..asmdef +│ └── /*.cs +├── Tests/ +│ ├── Editor/ +│ │ └── SashaRX..Tests.Editor.asmdef +│ └── Runtime/ (optional) +│ └── SashaRX..Tests.Runtime.asmdef +├── Samples~/ +│ └── / +├── Documentation~/ +│ ├── index.md +│ └── images/ +└── Native~/ (optional, C++ sources excluded from AssetDatabase) +``` + +Rules: + +- `Editor/` and `Runtime/` sit at the package root; never nest them inside each other. +- Tilde-hidden folders (`Samples~`, `Documentation~`, `Tests~`, `Native~`) have no `.meta` files and are ignored by AssetDatabase. +- `Tests/` without a tilde compiles into the consumer project if they import the package; `Tests~/` ships tests that stay hidden unless imported explicitly. + +## `package.json` field-by-field + +```json +{ + "name": "com.sasharx.", + "version": "1.2.3", + "displayName": "", + "description": "Single-paragraph description; shown in Package Manager.", + "unity": "2021.3", + "unityRelease": "0f1", + "documentationUrl": "https://github.com/SashaRX/", + "changelogUrl": "https://github.com/SashaRX//blob/main/CHANGELOG.md", + "licensesUrl": "https://github.com/SashaRX//blob/main/LICENSE", + "repository": { "type": "git", "url": "https://github.com/SashaRX/.git" }, + "author": { "name": "SashaRX" }, + "license": "MIT", + "dependencies": { + "com.unity.editorcoroutines": "1.0.0" + }, + "samples": [ + { "displayName": "Basic", "description": "Minimum example", "path": "Samples~/Basic" } + ] +} +``` + +Field rules: + +- `name` — reverse-DNS, all lowercase; `com.sasharx.`. Must match the package folder name (lowercased). +- `version` — SemVer; matches the git tag `v`. +- `unity` — the minimum supported LTS (`"2021.3"` / `"2022.3"` / `"6000.0"`). No patch. +- `unityRelease` — optional release modifier, e.g., `"0f1"`. Omit unless you require a specific patch. +- `displayName`, `description` — shown in Package Manager; keep under 200 chars. +- `repository`, `documentationUrl`, `changelogUrl`, `licensesUrl` — populate when the repo is public. +- `dependencies` — other UPM packages with SemVer ranges. Do not list Unity modules. +- `samples` — one entry per folder under `Samples~/`; entries appear in Package Manager as Import buttons. + +Non-standard fields (`type`, `main`, `module`) must not appear. + +## Asmdef pair pattern + +Every package ships at least two asmdefs — Runtime and Editor — and at least one test asmdef. + +**Runtime asmdef** (`Runtime/SashaRX..asmdef`): + +```json +{ + "name": "SashaRX.", + "rootNamespace": "SashaRX.", + "references": [], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} +``` + +**Editor asmdef** (`Editor/SashaRX..Editor.asmdef`): + +```json +{ + "name": "SashaRX..Editor", + "rootNamespace": "SashaRX..Editor", + "references": [ "SashaRX." ], + "includePlatforms": [ "Editor" ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "autoReferenced": false, + "defineConstraints": [], + "versionDefines": [] +} +``` + +**Tests (Editor) asmdef** (`Tests/Editor/SashaRX..Tests.Editor.asmdef`): + +```json +{ + "name": "SashaRX..Tests.Editor", + "rootNamespace": "SashaRX..Tests", + "references": [ + "SashaRX.", + "SashaRX..Editor", + "UnityEngine.TestRunner", + "UnityEditor.TestRunner" + ], + "includePlatforms": [ "Editor" ], + "defineConstraints": [ "UNITY_INCLUDE_TESTS" ], + "optionalUnityReferences": [ "TestAssemblies" ] +} +``` + +Each asmdef file basename equals the `name` field. `rootNamespace` matches the namespace block of every `.cs` file beneath. + +## `versionDefines` vs `defineConstraints` + +| Field | Role | +|---|---| +| `versionDefines` | Conditionally defines a symbol when a dependency or Unity version matches the expression. | +| `defineConstraints` | Rejects compilation unless every listed symbol is defined. | + +Example: gate code on the presence of the FBX exporter package. + +```json +"versionDefines": [ + { "name": "com.unity.formats.fbx", "expression": "[5.0.0,6.0.0)", "define": "LIGHTMAP_UV_TOOL_FBX_EXPORTER" } +] +``` + +Expression syntax follows NuGet interval notation. See `_shared/version-gates.md` for the full recipes. + +## `Samples~` and the `samples` array + +Every `Samples~//` folder corresponds to one entry in the `samples` array with `path: "Samples~/"`. Unity imports the folder into `Assets/Samples///` on demand. + +Rules: + +- Sample folder names are PascalCase without spaces. +- Each sample contains a small `README.md`. +- Samples that need a scene reference it with a forward-slash relative path only. + +## `Documentation~` and `Tests~` conventions + +- `Documentation~` is the canonical docs folder; consumers never see it compiled. +- `Tests~` is used for tests that ship but should NOT compile in consumer projects by default. `Tests/` without the tilde is used when tests should run as part of the consumer's Test Runner. + +## `Native~` + +Native source (C/C++) that produces platform binaries. Binaries are committed to `Plugins/` with the appropriate platform filter in their `.meta`. Keep sources in `Native~` so they do not participate in AssetDatabase. + +## `.gitignore` and `.npmignore` + +- Always commit `.meta` files. Never add a broad `*.meta` rule to `.gitignore`. +- Ignore `Library/`, `Logs/`, `Temp/`, `UserSettings/` at the repository root. +- For UPM publishing, `.npmignore` excludes `Documentation~/` and `Tests~/` from the tarball if you want smaller packages; however, canonical SashaRX packages ship everything so `.npmignore` stays empty. + +## Good vs bad pattern pairs + +**Bad: flat `Assets/`-style layout** + +``` +MyPackage/ +├── package.json +└── Assets/ + ├── Scripts/ + └── Editor/ +``` + +**Good: canonical tree above.** + +**Bad: single asmdef containing both Editor and Runtime code.** + +**Good: Runtime + Editor pair with the Editor asmdef referencing the Runtime asmdef.** + +**Bad: `"unity": "2022"` (no minor).** + +**Good: `"unity": "2022.3"`.** + +## Further reading + +- `_shared/naming-conventions.md` +- `_shared/version-gates.md` +- `_checklists/package-release.md` +- `unity-package-reviewer/SKILL.md` +- `unity-package-bootstrap/SKILL.md` diff --git a/.claude/skills/unity-package-reviewer/SKILL.md b/.claude/skills/unity-package-reviewer/SKILL.md index cf24039b..2bfb630c 100644 --- a/.claude/skills/unity-package-reviewer/SKILL.md +++ b/.claude/skills/unity-package-reviewer/SKILL.md @@ -1,8 +1,170 @@ --- name: unity-package-reviewer -description: Используй при ревью диффа, PR или существующего файла на нарушения правил Unity-пакета. Указывает конкретный скилл, чьё правило нарушено. +description: Audit a Unity UPM package for release readiness — verify package.json, asmdef platform filters, CHANGELOG conformance, SemVer bump correctness, Samples~ wiring, and absence of Assets/-only references. Use when preparing a release, merging a version bump PR, reviewing a UPM package for publication, or inspecting package.json changes. Runs through _checklists/package-release.md end to end. +paths: ["**/package.json", "**/CHANGELOG.md"] --- -# Unity Package Reviewer +# unity-package-reviewer -CRITICAL: .mesh, DestroyImmediate без Undo, мутация prefab без scope, StartAssetEditing без try/finally, мутация без Undo, target cast, Editor в Runtime. HIGH: ручной JSON, нет timeout, забытый ApplyModifiedProperties, нет bypass, >50КБ, хардкод. LOW: нет #if, нет progress bar, namespace без префикса. +Review skill for Unity UPM packages at release time. The complementary author skill is `unity-package-architect`. This skill produces a findings report; it does not author content. For every finding, it cites the file, the line number, and the skill whose rule was violated. + +## Scope and delegations + +Covered here: + +- SemVer correctness relative to diff since the previous tag. +- `package.json` fields and consistency with repository metadata. +- Asmdef platform filter audit. +- `samples` array vs `Samples~/` folder cross-check. +- CHANGELOG format and coverage. +- Missing docs or license. + +Delegated elsewhere: + +- **Code-level mutation safety** → `unity-undo-prefab-safety` finding cited by this reviewer. +- **Batching safety** → `unity-assetdatabase-tools` finding cited by this reviewer. +- **Migration execution** → `migration-and-refactor-planner`. +- **End-to-end release checklist** → `_checklists/package-release.md`. + +## Review phases + +Work through the phases in order. Each phase corresponds to a section of `_checklists/package-release.md`. + +1. **SemVer decision** +2. **CHANGELOG coverage** +3. **`package.json` audit** +4. **Asmdef audit** +5. **Documentation and license** +6. **Build and tests** +7. **Tag and publish readiness** + +Each phase ends with a findings subsection in the final report, severity-tagged. + +## SemVer decision tree + +| Change | Bump | +|---|---| +| Internal refactor, docstring, typo | PATCH | +| Bug fix with no API change | PATCH | +| New public type, new asmdef, new sample | MINOR | +| New optional `dependencies` entry | MINOR | +| New `versionDefines` symbol consumed elsewhere | MINOR | +| Public API removed or renamed | MAJOR | +| `unity` minimum bumped | MAJOR | +| Namespace changed | MAJOR | +| Asmdef name changed | MAJOR | + +Algorithm: + +1. `git log --oneline ..HEAD` to list commits. +2. `git diff ..HEAD --stat` to identify changed files. +3. For each `.cs` file changed, classify: internal / additive / removal-or-rename. +4. For each `.asmdef` / `package.json` / `Samples~/` change, classify similarly. +5. Highest classification wins; map to PATCH / MINOR / MAJOR. + +Report: whether the proposed `version` in `package.json` matches the classification. + +## CHANGELOG coverage + +Required format: Keep-a-Changelog (`Added` / `Changed` / `Deprecated` / `Removed` / `Fixed` / `Security` sections; one `## [version] — YYYY-MM-DD` header per release). + +Checks: + +- The new version's header appears at the top of `CHANGELOG.md`. +- The header date is today or a recent date. +- Every commit since the previous tag maps to at least one CHANGELOG line, OR is explicitly internal (test-only, docs-only, tooling). +- There is no stale `## [Unreleased]` block that contradicts the new header. + +## `package.json` audit + +- `name` is `com.sasharx.` and lowercased. +- `version` matches the CHANGELOG header and will match the git tag. +- `unity` equals or moves up from the previous tagged value; downgrades are a defect unless explicitly justified in CHANGELOG. +- `displayName` and `description` non-empty and under 200 chars. +- `repository.url` equals `git remote get-url origin` value. +- `dependencies` entries resolve; no stale entries. +- No non-standard fields (`type`, `main`, `module`). +- `samples[]` corresponds one-to-one to `Samples~/` folders. + +## Asmdef audit + +- Runtime asmdefs have empty `includePlatforms` and empty `excludePlatforms`. +- Editor asmdefs have `includePlatforms: ["Editor"]`. +- Test asmdefs include `defineConstraints: ["UNITY_INCLUDE_TESTS"]` and reference `UnityEngine.TestRunner` + `UnityEditor.TestRunner`. +- No Runtime asmdef references an Editor asmdef. +- `rootNamespace` matches the namespace used in every `.cs` file under the asmdef. +- Asmdef file basename equals `name` field. + +## Documentation and license + +- `README.md` install-via-git-URL example references the default branch or a tagged version. +- `LICENSE` SPDX identifier matches `license` in `package.json`. +- `Documentation~/` renders if present. + +## Build and tests + +- EditMode tests are present under `Tests/Editor/` or `Tests~/Editor/`. +- CI status on the target branch is green on every matrix row. +- Every `#if UNITY_*` directive has a compiling `#else` branch. + +## Tag and publish readiness + +- The proposed git tag name is `v` and does not already exist. +- The tag target is HEAD of the default branch after the version bump commit. + +## Report format + +Emit Markdown with this structure. Every finding includes a severity, a file path with line number when applicable, and the skill whose rule was violated. + +``` +# Review: SashaRX/ at + +## SemVer +- OK / FAIL: … + +## CHANGELOG +- [severity] file:line — … (skill: unity-package-reviewer) + +## package.json +- [severity] package.json:NN — … + +## Asmdef +- [severity] Editor/.asmdef:NN — … (skill: unity-package-architect) + +## Documentation and license +- … + +## Build and tests +- … + +## Tag and publish +- … + +## Summary +- critical: N, warning: N, info: N +- verdict: READY / NOT-READY +``` + +Severity levels: `critical` (blocks release), `warning` (ship after fix), `info` (defer). + +## Good vs bad finding pairs + +**Bad:** + +``` +- CHANGELOG doesn't match version +``` + +**Good:** + +``` +- [critical] CHANGELOG.md:3 — header reads "## [1.2.3]" but package.json version is "1.3.0" + (skill: unity-package-reviewer) +``` + +## Further reading + +- `_checklists/package-release.md` +- `unity-package-architect/SKILL.md` +- `_shared/naming-conventions.md` +- `_shared/anti-patterns.md` (items 16–20) From 21f6f44f844892b5ab6632b1c7ad1d996bb8ae58 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Apr 2026 07:46:24 +0000 Subject: [PATCH 06/21] docs(skills): rewrite migration-planner + repo-auditor with narrower scope --- .../migration-and-refactor-planner/SKILL.md | 156 +++++++++++++++++- .claude/skills/repo-auditor/SKILL.md | 139 +++++++++++++++- 2 files changed, 289 insertions(+), 6 deletions(-) diff --git a/.claude/skills/migration-and-refactor-planner/SKILL.md b/.claude/skills/migration-and-refactor-planner/SKILL.md index 1e9bd8ba..a491526a 100644 --- a/.claude/skills/migration-and-refactor-planner/SKILL.md +++ b/.claude/skills/migration-and-refactor-planner/SKILL.md @@ -1,8 +1,158 @@ --- name: migration-and-refactor-planner -description: Используй при планировании многошаговых рефакторингов — декомпозиция монолитных файлов, смена минимальной версии Unity, переименование namespace, добавление Tests~/Documentation~/Samples~. +description: Plan a safe migration inside a Unity UPM package — namespace rename, asmdef restructure, Unity minimum-version bump, or API deprecation. Use when renaming a namespace across many files, splitting or merging asmdefs, bumping the "unity" field in package.json, or deprecating public API that downstream samples or tests depend on. Always sequence snapshot then migrate then regenerate GUIDs only if unavoidable then run tests then bump SemVer. --- -# Migration & Refactor Planner +# migration-and-refactor-planner -Плейбуки: декомпозиция монолитов (по блоку за коммит), добавление тестов (Tests/Editor + asmdef + smoke), извлечение в UPM (структура + namespace + git URL), смена версии Unity, ренейм namespace. Каждый шаг = отдельный коммит. +Planner skill for intra-package migrations. Scope is bounded to a single UPM package; cross-repo refactors are out of scope. Every migration follows the same phase template: snapshot, discover, plan, execute, validate, commit, bump. + +## Scope and delegations + +Covered here: + +- Namespace rename across `.cs` files and asmdef `rootNamespace` / `name`. +- Asmdef split and merge with GUID preservation. +- Unity minimum-version bump with API audit. +- Public API deprecation via `[Obsolete]` progression. +- Adding or moving `Tests/`, `Samples~/`, `Documentation~/`. + +Delegated elsewhere: + +- **New package from scratch** → `unity-package-bootstrap`. +- **Pre-release audit** → `unity-package-reviewer`. +- **Per-repo conventions that drive the migration target** → `repo-conventions`. +- **Repository-wide audit** → `repo-auditor`. + +## Phase template (use for every migration) + +1. **Snapshot**: `git tag -a pre--` on clean HEAD. Creates the rollback target. +2. **Discover**: run a read-only scan to catalogue every affected file; write the list to `/tmp/-files.txt`. +3. **Plan**: write the migration steps to `/tmp/-plan.md` with one step per commit. +4. **Execute**: one commit per step; verify compile between steps. +5. **Validate**: run tests in the editor and CI; confirm no downstream consumers broke. +6. **Commit**: squash-or-merge only after validation. +7. **Bump**: update `CHANGELOG.md` and `package.json` `version` per SemVer, tag the release. + +Rollback at any step: `git reset --hard pre--` and `git tag -d pre--`. + +## Playbook: namespace rename + +Target: move every namespace from `` to `SashaRX.`. + +Discovery: + +```bash +grep -rn "^namespace $OLD" --include='*.cs' | tee /tmp/ns-rename-files.txt +git ls-files '*.asmdef' | tee /tmp/ns-rename-asmdefs.txt +``` + +Execute: + +- Replace namespace blocks in every listed `.cs` file; word-boundary-safe replacement only. +- Update every `using $OLD…;` to `using $NEW…;`. +- Update each asmdef's `rootNamespace` to the new root; update `name` if it encoded the old namespace. +- Keep the asmdef file name unchanged on disk until the last step to preserve GUIDs. Rename the asmdef file as a final, isolated commit. + +Validate: + +- Compile in the minimum supported Unity version. +- Run EditMode tests. +- Verify `git grep "^namespace $OLD"` returns nothing. +- Verify no unresolved `using` directives in the CS compiler output. + +Commit plan (one commit each): + +1. Replace namespace blocks in `Runtime/`. +2. Replace namespace blocks in `Editor/`. +3. Replace namespace blocks in `Tests/`. +4. Update `using` directives across all three. +5. Update asmdef `rootNamespace`. +6. Rename asmdef file (if applicable) via `AssetDatabase.RenameAsset` in editor, or direct rename + `.meta` preservation; commit the rename as a single-file change so GUID preservation is obvious in review. + +## Playbook: asmdef split or merge + +Rule: **never delete an asmdef and recreate with the same name**. The GUID is lost; references in other asmdefs silently drop. + +Split: + +1. Create the new asmdef file; copy `references` minus the lines that move out. +2. Move `.cs` files to the new asmdef's folder via `AssetDatabase.MoveAsset` (preserves GUIDs). +3. Update references in downstream asmdefs to point at the new name. +4. Commit each step. + +Merge: + +1. Pick one asmdef as the survivor; keep its file (GUID stable). +2. Move `.cs` from the other asmdef(s) into the survivor's folder. +3. Update references in downstream asmdefs: remove the merged asmdef, keep the survivor. +4. Delete the merged asmdef file last; its `.meta` goes with it. + +## Playbook: Unity minimum-version bump + +Target: raise `package.json` `unity` from `` to ``. + +Discovery: + +```bash +grep -rn "#if UNITY_" --include='*.cs' Editor Runtime | tee /tmp/unity-bump-gates.txt +git grep -n "UNITY_[0-9_]*_OR_NEWER" -- Editor Runtime +``` + +Audit: + +- Identify APIs used in the codebase that were added after `` but before `` — those gates are now unconditional and the `#else` branches can be removed. +- Identify APIs deprecated between `` and `` — replace with current equivalents. +- Update CI matrix to include `` and drop rows below ``. + +Execute: + +1. Update `package.json`: `"unity": ""`. +2. Remove unnecessary `#if UNITY_…_OR_NEWER` gates whose threshold is `<= `. +3. Replace deprecated APIs. +4. Update CI matrix in `.github/workflows/*.yml`. +5. Add a CHANGELOG `## Changed` entry calling out the new minimum. + +SemVer: this is a MAJOR bump. + +## Playbook: public API deprecation + +Phased `[Obsolete]` progression; never remove a public symbol in one step. + +1. Release N: add `[Obsolete("Use NewApi instead.", error: false)]`. CHANGELOG `## Deprecated`. +2. Release N+1 (next MINOR): keep deprecation warning; add `NewApi` and document the migration path. CHANGELOG `## Added` for `NewApi`. +3. Release N+2 (next MINOR): change to `error: true`. CHANGELOG `## Deprecated` with removal target. +4. Release N+3 (next MAJOR): remove the symbol. CHANGELOG `## Removed`. + +Rule: samples and tests that reference the deprecated symbol must be updated in step 2. + +## Playbook: adding `Tests/`, `Samples~/`, `Documentation~/` + +1. Create the folder. +2. Create the asmdef (for `Tests/Editor/`). +3. Add a single smoke test or a placeholder `README.md`. +4. Update `package.json` `samples` array if adding `Samples~/`. +5. Commit each step. + +## Validation rules + +After any migration, run: + +- `git grep -n "^namespace" -- Editor Runtime Tests` and confirm no stale namespaces. +- `jq '.name, .rootNamespace, .references' **/*.asmdef` and confirm consistency. +- `jq '.unity' package.json` against the CI matrix declared in `.github/workflows/*.yml`. +- EditMode tests green on minimum Unity version and current LTS. + +## Good vs bad plan pairs + +**Bad:** single commit renaming namespace in 40 files across Runtime, Editor, and Tests plus renaming two asmdef files plus updating three downstream references. + +**Good:** six commits matching the "Commit plan" list above, each reviewable and revertable independently. + +## Further reading + +- `_checklists/package-release.md` +- `_shared/version-gates.md` +- `_shared/naming-conventions.md` +- `unity-package-architect/SKILL.md` +- `unity-package-reviewer/SKILL.md` diff --git a/.claude/skills/repo-auditor/SKILL.md b/.claude/skills/repo-auditor/SKILL.md index 539e241a..a2ccd3b8 100644 --- a/.claude/skills/repo-auditor/SKILL.md +++ b/.claude/skills/repo-auditor/SKILL.md @@ -1,8 +1,141 @@ --- name: repo-auditor -description: Используй при запросе аудита, сканирования или отчёта о здоровье Unity UPM-пакета. Только отчёт, без правок. +description: Audit a Unity UPM repository's health — skill directory structure, AGENTS.md/CLAUDE.md coherence, CI workflow presence, LICENSE, CHANGELOG, package.json correctness, .gitignore safety. Use when onboarding a new repo, before a release, or when asked to "audit", "review the repo", or "check project health". Produces a prioritized findings report with line-referenced citations. --- -# Repo Auditor +# repo-auditor -Сканируй репо и выдавай таблицу OK/WARN/FAIL по: package.json, asmdef, структура, namespace, размеры файлов, README/CHANGELOG, LICENSE, CI, антипаттерны. Не пиши фиксы. +Read-only audit skill for Unity UPM repositories. Produces a prioritized Markdown report; never edits files. Findings are actionable — each cites the file, the line number when applicable, and the skill that owns the rule being checked so the reader can route a fix to the right author skill. + +## Scope and delegations + +Covered here: + +- Skills directory structure and coherence with `.claude/skills/skills-overhaul-plan.md` if present. +- Agent docs (`AGENTS.md`, `CLAUDE.md`) presence and consistency with each other. +- `.github/workflows/` presence for Unity CI and release automation. +- `LICENSE`, `CHANGELOG.md`, `README.md` presence and format. +- `package.json` correctness (delegates per-field checks to `unity-package-reviewer`). +- Asmdef platform filter sanity (delegates to `unity-package-reviewer`). +- `.gitignore` safety (no `*.meta` rule, no `Library/` commit). +- `Samples~` / `Documentation~` presence when declared. + +Delegated elsewhere: + +- **Per-release audit** → `unity-package-reviewer` and `_checklists/package-release.md`. +- **Migration planning from findings** → `migration-and-refactor-planner`. +- **Rule authority** → each finding cites the skill owning the rule. + +## Audit dimensions + +| Dimension | Checks | +|---|---| +| Skills | `.claude/skills/` exists, contains canonical skills, `_shared/` and `_checklists/` populated, no orphan references, every skill has valid frontmatter | +| Agent docs | `AGENTS.md` or `CLAUDE.md` present; if both, they do not contradict; neither duplicates skill content | +| CI workflows | `.github/workflows/*.yml` includes a test workflow targeting the minimum Unity version in `package.json` | +| License | `LICENSE` file at repo root; SPDX matches `package.json` `license` | +| Changelog | `CHANGELOG.md` follows Keep-a-Changelog; latest version matches `package.json` `version` | +| Readme | `README.md` present; install-via-git-URL example cites default branch or a tagged version | +| package.json | Reverse-DNS name, SemVer version, `unity` LTS, no non-standard fields | +| Asmdef | Runtime/Editor pair present, platform filters correct, `rootNamespace` consistent | +| Namespace | Every `.cs` file uses `SashaRX.` (or documented deviation in `repo-conventions`) | +| .gitignore | No `*.meta` rule; `Library/`, `Logs/`, `Temp/`, `UserSettings/` ignored | +| Samples | Every `samples[]` entry in `package.json` has a matching `Samples~//` folder | +| Documentation | `Documentation~/` exists if referenced by `documentationUrl` field | + +## Report format + +``` +# Audit: SashaRX/ at + +## Summary +- critical: N, warning: N, info: N +- verdict: CLEAN / NEEDS-WORK / BROKEN + +## Skills +- [severity] .claude/skills//SKILL.md:NN — (skill: ) + +## Agent docs +- … + +## CI workflows +- … + +## License, Changelog, Readme +- … + +## package.json +- … (skill: unity-package-reviewer) + +## Asmdef +- … (skill: unity-package-architect) + +## Namespace +- … (skill: unity-package-architect / repo-conventions) + +## .gitignore +- … + +## Samples and Documentation +- … + +## Recommended follow-up +- Route `migration-and-refactor-planner` for: +``` + +Severity levels: + +- `critical` — blocks release or development (no CI, no LICENSE, namespace non-compliance). +- `warning` — ship after fix (missing sample README, outdated CHANGELOG header). +- `info` — defer (style drift, optional folders absent). + +## Discovery commands + +The skill runs these commands (read-only) to build the report: + +```bash +test -f package.json && jq -r '.name, .version, .unity, .license' package.json +test -f LICENSE && head -3 LICENSE +test -f CHANGELOG.md && head -20 CHANGELOG.md +test -f README.md && head -40 README.md +test -d .claude/skills && find .claude/skills -maxdepth 2 -name SKILL.md | sort +test -d .github/workflows && ls .github/workflows +git grep -n "^namespace " -- 'Editor/*.cs' 'Runtime/*.cs' | head -20 +git ls-files '*.asmdef' +git check-ignore -v Library/ Logs/ Temp/ UserSettings/ 2>/dev/null +grep -n '^\*\.meta$' .gitignore 2>/dev/null +``` + +## Integration with other skills + +Every finding should name a follow-up target: + +- Skills directory findings → this skill (`repo-auditor`) and `skills-overhaul-plan.md` if present. +- Agent doc findings → manual resolution; this skill does not author. +- CI findings → `unity-ci-validation`. +- package.json / asmdef findings → `unity-package-reviewer` for audit, `unity-package-architect` for fix. +- Namespace findings → `migration-and-refactor-planner`. +- Bootstrap findings (missing package skeleton) → `unity-package-bootstrap`. + +## Good vs bad finding pairs + +**Bad:** + +``` +- There's no license. +``` + +**Good:** + +``` +- [critical] LICENSE — missing; package.json:7 declares "license": "MIT" but no LICENSE file exists. + Route: add MIT LICENSE via unity-package-bootstrap template. +``` + +## Further reading + +- `_checklists/package-release.md` +- `_shared/anti-patterns.md` +- `unity-package-reviewer/SKILL.md` +- `unity-package-architect/SKILL.md` +- `migration-and-refactor-planner/SKILL.md` From ed6c7f68bc5b895445bbb55af304a0d33944d75f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Apr 2026 07:48:43 +0000 Subject: [PATCH 07/21] feat(skills): add unity-ci-validation, unity-package-bootstrap, and per-repo repo-conventions --- .claude/skills/repo-conventions/SKILL.md | 79 +++++++ .claude/skills/unity-ci-validation/SKILL.md | 222 ++++++++++++++++++ .../skills/unity-package-bootstrap/SKILL.md | 108 +++++++++ 3 files changed, 409 insertions(+) create mode 100644 .claude/skills/repo-conventions/SKILL.md create mode 100644 .claude/skills/unity-ci-validation/SKILL.md create mode 100644 .claude/skills/unity-package-bootstrap/SKILL.md diff --git a/.claude/skills/repo-conventions/SKILL.md b/.claude/skills/repo-conventions/SKILL.md new file mode 100644 index 00000000..a452033f --- /dev/null +++ b/.claude/skills/repo-conventions/SKILL.md @@ -0,0 +1,79 @@ +--- +name: repo-conventions +description: Canonical conventions for THIS repository (UnityMeshLab) — the package ID, namespace, Unity version target, CI workflow names, and any deviations from _shared/naming-conventions.md. Use at the start of any non-trivial task in this repo, when creating new C# files (to pick the correct namespace), when adding asmdefs, or when editing package.json. Overrides _shared/* where they conflict. +--- + +# repo-conventions (UnityMeshLab) + +Canonical conventions for the `UnityMeshLab` repository. This file overrides `_shared/naming-conventions.md` where an explicit deviation is documented. Read this first when creating new files, adding asmdefs, or editing `package.json`. + +## Identity (canonical target) + +| Field | Canonical value | +|---|---| +| Repository | `SashaRX/UnityMeshLab` | +| Display name | `Mesh Lab` | +| Package ID | `com.sasharx.unitymeshlab` | +| Root namespace | `SashaRX.UnityMeshLab` | +| Unity minimum version | `6000.0` (Unity 6) | +| Default branch | `main` | + +## Assemblies + +| Role | Asmdef name | `rootNamespace` | Platforms | +|---|---|---|---| +| Runtime | `SashaRX.UnityMeshLab` | `SashaRX.UnityMeshLab` | all | +| Editor | `SashaRX.UnityMeshLab.Editor` | `SashaRX.UnityMeshLab.Editor` | `Editor` only | +| Tests (Editor) | `SashaRX.UnityMeshLab.Tests.Editor` | `SashaRX.UnityMeshLab.Tests` | `Editor` only | + +## CI workflows + +Located under `.github/workflows/`: + +- `build-native.yml` — builds native plugin binaries (platform-specific). +- `meta-check.yml` — verifies `.meta` file coverage. +- `version-bump.yml` — automates `package.json` version bumps. + +Missing and planned (see `unity-ci-validation/SKILL.md`): + +- `test.yml` — EditMode/PlayMode matrix on 6000.0 minimum. +- `release.yml` — tag-triggered GitHub Release. + +## Deviations from `_shared/naming-conventions.md` + +Both items below are known deviations captured for tracking; the migration to the canonical values is planned (see "Migration status" below). + +- **`package.json` `name`** is currently `com.sasharx.lightmap-uv-tool` — does NOT match the repository folder name `UnityMeshLab`. Canonical value: `com.sasharx.unitymeshlab`. Rationale for deviation: historical — the repo originated as `lightmap-uv-tool` before the mesh-lab rename. Migration breaks downstream consumers; schedule under a MAJOR SemVer bump. +- **Root namespace** is currently `LightmapUvTool` (bare single-segment) in `.cs` files under `Editor/`. Canonical value: `SashaRX.UnityMeshLab`. Rationale for deviation: none; pre-dates the two-segment rule. Migration tracked via `migration-and-refactor-planner`. + +## Primary domain vocabulary + +Terms that identify tasks as in-scope for this repo (used as description triggers elsewhere): + +- Mesh editor, mesh hygiene, mesh repacking. +- Lightmap UV, UV2, baked lightmap, UV transfer. +- LOD group, LOD UV workflow, LOD sibling detection. +- FBX export (gated by `LIGHTMAP_UV_TOOL_FBX_EXPORTER`). +- Sidecar asset (`Uv2DataAsset` — persists UV2/collision data next to FBX). + +## Repo-specific rules (from CLAUDE.md) + +Shared with agents via `CLAUDE.md`: + +- No `using System.Text.RegularExpressions` in `LightmapTransferTool.cs` — use fully qualified `System.Text.RegularExpressions.Regex`. +- Log via `UvtLog.Info` / `UvtLog.Warn` / `UvtLog.Error` — prefixed `[LightmapUV]`. +- Use `Undo.RecordObject` / `Undo.AddComponent` / `Undo.DestroyObjectImmediate` for scene modifications. +- Call `RestoreWorkingMeshes()` before clearing/switching LODGroup context. +- Destroy temporary meshes (repacked, transferred, welded) when no longer needed. + +## Migration status + +Tracked rename of `LightmapUvTool` → `SashaRX.UnityMeshLab` is scheduled under `migration-and-refactor-planner`. Until complete, new code in `Editor/` and `Runtime/` must still use the current bare namespace to avoid mixing conventions mid-migration. After the migration lands, this section will be removed and the Deviations section updated. + +## Further reading + +- `_shared/naming-conventions.md` +- `_shared/version-gates.md` +- `migration-and-refactor-planner/SKILL.md` +- `unity-package-architect/SKILL.md` +- `unity-ci-validation/SKILL.md` diff --git a/.claude/skills/unity-ci-validation/SKILL.md b/.claude/skills/unity-ci-validation/SKILL.md new file mode 100644 index 00000000..e24f2810 --- /dev/null +++ b/.claude/skills/unity-ci-validation/SKILL.md @@ -0,0 +1,222 @@ +--- +name: unity-ci-validation +description: Author and debug Unity CI workflows for UPM packages — GameCI actions, batch mode EditMode and PlayMode test runs, license activation, NUnit result parsing, and release gates on SemVer. Use when creating or editing .github/workflows/*.yml for Unity, diagnosing a failed CI run, or wiring semantic-release / GitHub Release automation. Knows -batchmode -nographics -runTests -testPlatform -testResults conventions and Application.isBatchMode guards. +paths: [".github/workflows/**/*.yml", ".github/workflows/**/*.yaml"] +--- + +# unity-ci-validation + +Authoring and debugging skill for Unity CI. Covers GameCI workflows, batch-mode invocations, license activation, and release automation. This skill does not edit C# code; use `unity-editor-tooling` for that. The companion audit is `repo-auditor`. + +## Scope and delegations + +Covered here: + +- `.github/workflows/*.yml` skeletons for test and release. +- GameCI action versions and inputs. +- License activation (personal and professional). +- Batch-mode command-line flags. +- NUnit XML result parsing and GitHub Check annotations. +- Release automation via semantic-release or manual tag push. + +Delegated elsewhere: + +- **Package metadata checks** → `unity-package-reviewer`. +- **Code-level test authoring** → `unity-serialized-workflow`, `unity-editor-tooling`. +- **Namespace migration** → `migration-and-refactor-planner`. + +## GameCI skeleton (test + release) + +Pin action versions to exact tags. Floating-version references are a common CI-flake source. + +```yaml +name: ci +on: + push: + branches: [main] + tags: ['v*'] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + unity-version: ['2021.3.45f1', '2022.3.50f1', '6000.0.33f1'] + test-mode: ['editmode'] + steps: + - uses: actions/checkout@v4 + with: { lfs: true } + + - uses: actions/cache@v4 + with: + path: Library + key: Library-${{ matrix.unity-version }}-${{ hashFiles('Packages/**/*.json', '**/*.asmdef') }} + restore-keys: | + Library-${{ matrix.unity-version }}- + Library- + + - uses: game-ci/unity-test-runner@v4 + env: + UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} + with: + unityVersion: ${{ matrix.unity-version }} + testMode: ${{ matrix.test-mode }} + artifactsPath: artifacts/${{ matrix.unity-version }}-${{ matrix.test-mode }} + githubToken: ${{ secrets.GITHUB_TOKEN }} + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: test-results-${{ matrix.unity-version }}-${{ matrix.test-mode }} + path: artifacts/**/*.xml + + release: + needs: test + if: startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: softprops/action-gh-release@v2 + with: + generate_release_notes: true +``` + +Rules: + +- Matrix pins full Unity version strings (major.minor.patchNfN). Floating `2022.3` is not acceptable. +- Library cache key includes `*.asmdef` hash so asmdef churn invalidates cache automatically. +- `fail-fast: false` so one failing matrix row does not abort the others. +- Release job gates on `refs/tags/v*` — the test job runs on every push and PR, the release job only on tag push. + +## License activation + +Two modes: + +- **Personal license** — obtain an activation file via `game-ci/unity-request-activation-file`; encode to base64 and store in `UNITY_LICENSE` secret. Shown in the `UNITY_LICENSE` env above. +- **Professional license** — set `UNITY_SERIAL`, `UNITY_EMAIL`, and `UNITY_PASSWORD` secrets; `unity-test-runner` picks them up. + +Secrets layout (minimum scopes): + +- `UNITY_LICENSE` (personal) OR `UNITY_SERIAL` + `UNITY_EMAIL` + `UNITY_PASSWORD` (professional). +- `GITHUB_TOKEN` is automatic for standard actions. +- Never log secret contents. The GameCI action redacts by default; custom `run:` steps must not `echo` them. + +## Batch-mode invocations + +When a workflow invokes Unity directly (outside GameCI), use the canonical flag set: + +``` +unity -batchmode -nographics -quit \ + -projectPath "$GITHUB_WORKSPACE" \ + -logFile - \ + -executeMethod SashaRX..Editor.Ci.BuildExport.Run +``` + +Test runner mode: + +``` +unity -batchmode -nographics -quit \ + -projectPath "$GITHUB_WORKSPACE" \ + -runTests -testPlatform EditMode \ + -testResults artifacts/editmode.xml \ + -logFile - +``` + +Flags: + +- `-batchmode` disables interactive UI; required for CI. +- `-nographics` disables GPU initialization; required on headless runners. +- `-quit` exits after the method / test run completes. +- `-logFile -` streams the log to stdout so GitHub Actions captures it. +- `-testResults ` writes NUnit XML to the specified path. + +Exit codes: `0` success, `2` test failure, `3` build failure, `4` other failure. Treat any non-zero as failure unless explicitly mapped. + +## `Application.isBatchMode` guard + +Any `-executeMethod` entry point must guard against interactive dialogs. `EditorUtility.DisplayDialog` blocks indefinitely in batch mode unless gated. + +```csharp +public static class CiBuildExport +{ + public static void Run() + { + if (!Application.isBatchMode) + { + // Interactive path for local testing + RunInteractive(); + return; + } + + try { RunHeadless(); } + catch (Exception e) + { + Debug.LogError($"[CI] {e}"); + EditorApplication.Exit(1); + } + } +} +``` + +Do not call `EditorUtility.DisplayDialog`, `EditorUtility.DisplayCancelableProgressBar`, or any `System.Windows.Forms` API from a batch-mode code path. + +## NUnit result parsing + +GameCI's `unity-test-runner` emits NUnit 3 XML at the `artifactsPath`. To surface failures as GitHub Check annotations, consume the XML in a follow-up step: + +```yaml + - name: Annotate failing tests + if: always() + uses: dorny/test-reporter@v1 + with: + name: Unity ${{ matrix.unity-version }} ${{ matrix.test-mode }} + path: artifacts/**/*.xml + reporter: dotnet-trx +``` + +Alternative: upload the XML as an artifact (shown in the skeleton above) and download for post-mortem. + +## Release automation + +**Option A: manual tag push.** + +1. Maintainer bumps `package.json` `version` and `CHANGELOG.md`, commits, and pushes. +2. Maintainer tags: `git tag v && git push origin v`. +3. The `release` job gates on `refs/tags/v*` and creates a GitHub Release with generated notes. + +**Option B: semantic-release.** + +Use `cycjimmy/semantic-release-action@v4` with a Unity-specific plugin list in `.releaserc.json`: + +```json +{ + "branches": ["main"], + "plugins": [ + "@semantic-release/commit-analyzer", + "@semantic-release/release-notes-generator", + ["@semantic-release/changelog", { "changelogFile": "CHANGELOG.md" }], + ["@semantic-release/npm", { "npmPublish": false }], + ["@semantic-release/git", { "assets": ["CHANGELOG.md", "package.json"] }], + "@semantic-release/github" + ] +} +``` + +`"npmPublish": false` is mandatory — Unity packages are not published to npm; they are consumed via git URL or OpenUPM. + +## Debugging failed runs + +- **License activation failure** → check the secret's base64 encoding is single-line, no trailing newline. +- **Library cache miss on every run** → the cache key includes a hash of a file that changes every run; narrow the hash inputs. +- **Tests pass locally but fail in CI** → check `Application.isBatchMode` guards; check that tests do not depend on scene objects that exist only when the editor opens interactively. +- **`EditorApplication.Exit(0)` reached too early** → a test assembly failed to compile and batch mode short-circuits; check the log for `error CS`. +- **Release job runs but nothing publishes** → `npmPublish` left at `true` or tag format mismatched `v*`. + +## Further reading + +- `_checklists/package-release.md` +- `_shared/anti-patterns.md` (item 19) +- `unity-package-reviewer/SKILL.md` diff --git a/.claude/skills/unity-package-bootstrap/SKILL.md b/.claude/skills/unity-package-bootstrap/SKILL.md new file mode 100644 index 00000000..64eb0bf7 --- /dev/null +++ b/.claude/skills/unity-package-bootstrap/SKILL.md @@ -0,0 +1,108 @@ +--- +name: unity-package-bootstrap +description: Bootstrap a new Unity UPM package from the bundled _template/package-template/ — creates folder structure, substitutes {{PackageName}}, {{Namespace}}, {{DisplayName}}, {{UnityMinVersion}} placeholders, renames .template files, initializes git, and verifies the package imports cleanly. Use when the user asks to "create a new package", "scaffold a UPM package", "bootstrap a new Unity tool", or any request to start a new Unity editor extension. +--- + +# unity-package-bootstrap + +Scaffolding skill for new Unity UPM packages. Reads from `_template/package-template/`, substitutes placeholders, renames `.template` files, and initializes git. The shape it produces matches `unity-package-architect` exactly. + +## Scope and delegations + +Covered here: + +- Copying `_template/package-template/` to a target directory. +- Substituting `{{PackageName}}`, `{{Namespace}}`, `{{PackageId}}`, `{{DisplayName}}`, `{{Description}}`, `{{UnityMinVersion}}`, `{{Author}}`, `{{License}}`. +- Renaming `*.template` files to their final names (strips the `.template` suffix). +- Running `git init`, staging, creating an initial commit. +- Post-bootstrap verification. + +Delegated elsewhere: + +- **Package shape rules** → `unity-package-architect`. +- **CI workflow** → `unity-ci-validation`. +- **Release checks** → `unity-package-reviewer`. + +## Template inventory + +The template lives under `.claude/skills/_template/package-template/` relative to the repo that holds this skill. Contents: + +``` +_template/package-template/ +├── package.json.template +├── README.md.template +├── CHANGELOG.md.template +├── LICENSE.template +├── .gitignore.template +├── Editor/ +│ ├── SashaRX.{{PackageName}}.Editor.asmdef.template +│ └── PackageEditorEntryPoint.cs.template +├── Runtime/ +│ ├── SashaRX.{{PackageName}}.asmdef.template +│ └── PackageRuntimeEntryPoint.cs.template +├── Tests/Editor/ +│ └── SashaRX.{{PackageName}}.Tests.Editor.asmdef.template +├── Samples~/ +│ └── Basic/ +│ └── README.md.template +└── Documentation~/ + └── index.md.template +``` + +Every `.template` file contains `{{...}}` placeholders. The bootstrap pipeline walks the tree, substitutes, renames, and the user sees a clean package tree with no template artifacts. + +## Parameters + +Collect from the invoking user, with defaults: + +| Parameter | Default | Example | +|---|---|---| +| `PackageName` | required | `PrefabDoctor` (PascalCase) | +| `Namespace` | `SashaRX.{PackageName}` | `SashaRX.PrefabDoctor` | +| `PackageId` | `com.sasharx.{lowercase(PackageName)}` | `com.sasharx.prefabdoctor` | +| `DisplayName` | `{PackageName}` split on PascalCase boundaries | `Prefab Doctor` | +| `Description` | required | `Nested prefab override conflict finder.` | +| `UnityMinVersion` | `2021.3` | `2022.3` or `6000.0` | +| `Author` | `SashaRX` | | +| `License` | `MIT` | | + +Reject `PackageName` values that are not PascalCase, contain spaces, or equal a C# reserved word. + +## Execution sequence + +1. **Resolve target path.** Default: `/`. Fail if the directory already exists and is non-empty. +2. **Copy template tree.** Every file and folder is copied; `.template` suffixes remain on files at this point. +3. **Substitute placeholders.** For each file, replace every `{{Placeholder}}` token with the resolved value. Whole-word substitution only. +4. **Rename `.template` files.** Strip the `.template` suffix from every file name. Also rename folders that contain `{{PackageName}}` in their path (the asmdef folder is not one of these; the asmdef *file* name uses the namespace). +5. **Verify.** Parse `package.json` as JSON; parse each `.asmdef` as JSON; confirm no `{{...}}` markers remain anywhere under the new package. +6. **Initialize git.** `git init`, `git add .`, commit with message `chore: bootstrap from template`. +7. **Report.** Print the file tree, the resolved parameters, and the next steps (open in Unity, add CI via `unity-ci-validation`, add samples). + +## Post-bootstrap verification checklist + +- [ ] `jq . package.json >/dev/null` succeeds. +- [ ] `jq . Editor/*.asmdef >/dev/null` and `jq . Runtime/*.asmdef >/dev/null` succeed. +- [ ] `grep -rn '{{' .` returns nothing. +- [ ] `git status` shows a clean tree after the initial commit. +- [ ] File tree matches the canonical layout in `unity-package-architect/SKILL.md`. +- [ ] Namespace in `Editor/PackageEditorEntryPoint.cs` matches the `Namespace` parameter. + +## Integration with `unity-ci-validation` + +After the initial commit, prompt the user whether to add a GameCI workflow: + +- If yes, delegate to `unity-ci-validation` to author `.github/workflows/ci.yml` targeting `UnityMinVersion` and the current LTS. +- If no, add a `## CI` section to `README.md` pointing at `unity-ci-validation/SKILL.md` for later setup. + +## Good vs bad execution + +**Bad:** copy the template, open in Unity, manually edit `{{PackageName}}` in 12 files. + +**Good:** run the substitution + rename + verify pipeline in one pass; commit once; open in Unity to a ready package. + +## Further reading + +- `unity-package-architect/SKILL.md` +- `unity-ci-validation/SKILL.md` +- `_shared/naming-conventions.md` +- `_checklists/package-release.md` From ba05972e6cbfd891db48ebfced78b9ed9598b0c9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Apr 2026 07:50:48 +0000 Subject: [PATCH 08/21] refactor: migrate namespace LightmapUvTool -> SashaRX.UnityMeshLab (.cs files) Replaces 'namespace LightmapUvTool' declarations in 48 Editor/*.cs files with 'namespace SashaRX.UnityMeshLab' per the two-segment rule in .claude/skills/_shared/naming-conventions.md. String literals referencing 'LightmapUvTool' (shader paths, menu paths, asset folders, EditorPrefs keys) are intentionally left untouched; renaming those is a separate user-data-compat migration. --- Editor/BorderPrimitiveDetector.cs | 2 +- Editor/CheckerTexturePreview.cs | 2 +- Editor/CollisionMeshBuilder.cs | 2 +- Editor/ConvexDecompNative.cs | 2 +- Editor/CoverageSplitSolver.cs | 2 +- Editor/EdgeAnalyzer.cs | 2 +- Editor/Framework/IUvTool.cs | 2 +- Editor/Framework/MeshEntry.cs | 2 +- Editor/Framework/UvCanvasView.cs | 2 +- Editor/Framework/UvToolContext.cs | 2 +- Editor/Framework/UvToolHub.cs | 2 +- Editor/GroupedShellTransfer.cs | 2 +- Editor/MeshHygieneUtility.cs | 2 +- Editor/MeshOptimizer.cs | 2 +- Editor/MeshSimplifier.cs | 2 +- Editor/MeshoptNative.cs | 2 +- Editor/ModelBuilderPreview.cs | 2 +- Editor/PostprocessorDefineManager.cs | 2 +- Editor/Settings/MeshLabProjectSettings.cs | 2 +- Editor/ShellColorModelPreview.cs | 2 +- Editor/SpatialPartitioner.cs | 2 +- Editor/StripParameterization.cs | 2 +- Editor/SymmetrySplitShells.cs | 2 +- Editor/Tools/AtlasPackTool.cs | 2 +- Editor/Tools/CleanupTool.cs | 2 +- Editor/Tools/CollisionMeshTool.cs | 2 +- Editor/Tools/LightmapTransferTool.cs | 2 +- Editor/Tools/LodGenerationTool.cs | 2 +- Editor/Tools/ModelBuilderTool.cs | 2 +- Editor/Tools/Uv0OptimizeTool.cs | 2 +- Editor/Tools/VertexAOTool.cs | 2 +- Editor/TransferData.cs | 2 +- Editor/TransferValidator.cs | 2 +- Editor/TriangleBvh.cs | 2 +- Editor/TriangleBvh2D.cs | 2 +- Editor/Uv0Analyzer.cs | 2 +- Editor/Uv2AssetPostprocessor.cs | 2 +- Editor/Uv2DataAsset.cs | 2 +- Editor/UvMetricCalculator.cs | 2 +- Editor/UvShellExtractor.cs | 2 +- Editor/UvTransferPipeline.cs | 2 +- Editor/UvtLog.cs | 2 +- Editor/VertexAOBaker.Blur.cs | 2 +- Editor/VertexAOBaker.Cpu.cs | 2 +- Editor/VertexAOBaker.Gpu.cs | 2 +- Editor/VertexAOBaker.cs | 2 +- Editor/XatlasNative.cs | 2 +- Editor/XatlasRepack.cs | 2 +- 48 files changed, 48 insertions(+), 48 deletions(-) diff --git a/Editor/BorderPrimitiveDetector.cs b/Editor/BorderPrimitiveDetector.cs index 5dc79ad5..9e207bff 100644 --- a/Editor/BorderPrimitiveDetector.cs +++ b/Editor/BorderPrimitiveDetector.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using UnityEngine; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { public static class BorderPrimitiveDetector { diff --git a/Editor/CheckerTexturePreview.cs b/Editor/CheckerTexturePreview.cs index cbb27906..e6c89354 100644 --- a/Editor/CheckerTexturePreview.cs +++ b/Editor/CheckerTexturePreview.cs @@ -9,7 +9,7 @@ using UnityEditor; using UnityEditor.SceneManagement; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { /// /// Safety hook: restores all preview materials on domain reload, play mode change, diff --git a/Editor/CollisionMeshBuilder.cs b/Editor/CollisionMeshBuilder.cs index ed35eb7a..6c311d9d 100644 --- a/Editor/CollisionMeshBuilder.cs +++ b/Editor/CollisionMeshBuilder.cs @@ -6,7 +6,7 @@ using UnityEngine; using UnityEngine.Rendering; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { public static class CollisionMeshBuilder { diff --git a/Editor/ConvexDecompNative.cs b/Editor/ConvexDecompNative.cs index b8b651a5..b078e0af 100644 --- a/Editor/ConvexDecompNative.cs +++ b/Editor/ConvexDecompNative.cs @@ -3,7 +3,7 @@ using System; using System.Runtime.InteropServices; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { public static class ConvexDecompNative { diff --git a/Editor/CoverageSplitSolver.cs b/Editor/CoverageSplitSolver.cs index 1b15122b..307f0b82 100644 --- a/Editor/CoverageSplitSolver.cs +++ b/Editor/CoverageSplitSolver.cs @@ -13,7 +13,7 @@ using System.Collections.Generic; using UnityEngine; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { public static class CoverageSplitSolver { diff --git a/Editor/EdgeAnalyzer.cs b/Editor/EdgeAnalyzer.cs index 4d04acb0..b56cc170 100644 --- a/Editor/EdgeAnalyzer.cs +++ b/Editor/EdgeAnalyzer.cs @@ -10,7 +10,7 @@ using System.Collections.Generic; using UnityEngine; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { public static class EdgeAnalyzer { diff --git a/Editor/Framework/IUvTool.cs b/Editor/Framework/IUvTool.cs index a08bd474..8286f5c3 100644 --- a/Editor/Framework/IUvTool.cs +++ b/Editor/Framework/IUvTool.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using UnityEditor; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { /// /// Interface for pluggable tools in Mesh Lab. diff --git a/Editor/Framework/MeshEntry.cs b/Editor/Framework/MeshEntry.cs index acc5471a..dc302454 100644 --- a/Editor/Framework/MeshEntry.cs +++ b/Editor/Framework/MeshEntry.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using UnityEngine; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { /// /// Per-renderer mesh state shared across all tools. diff --git a/Editor/Framework/UvCanvasView.cs b/Editor/Framework/UvCanvasView.cs index 5e41bc54..90835ddb 100644 --- a/Editor/Framework/UvCanvasView.cs +++ b/Editor/Framework/UvCanvasView.cs @@ -7,7 +7,7 @@ using UnityEngine.Rendering; using UnityEditor; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { public class UvCanvasView { diff --git a/Editor/Framework/UvToolContext.cs b/Editor/Framework/UvToolContext.cs index eeb5c594..f06fceae 100644 --- a/Editor/Framework/UvToolContext.cs +++ b/Editor/Framework/UvToolContext.cs @@ -7,7 +7,7 @@ using UnityEngine; using UnityEditor; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { /// /// Shared state container — one instance per window. diff --git a/Editor/Framework/UvToolHub.cs b/Editor/Framework/UvToolHub.cs index c5baac4a..c70eb9cb 100644 --- a/Editor/Framework/UvToolHub.cs +++ b/Editor/Framework/UvToolHub.cs @@ -9,7 +9,7 @@ using UnityEngine; using UnityEditor; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { public class UvToolHub : EditorWindow { diff --git a/Editor/GroupedShellTransfer.cs b/Editor/GroupedShellTransfer.cs index 73e17607..18e1858b 100644 --- a/Editor/GroupedShellTransfer.cs +++ b/Editor/GroupedShellTransfer.cs @@ -16,7 +16,7 @@ using System.Collections.Generic; using UnityEngine; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { public static class GroupedShellTransfer { diff --git a/Editor/MeshHygieneUtility.cs b/Editor/MeshHygieneUtility.cs index 55ecd61b..624564d3 100644 --- a/Editor/MeshHygieneUtility.cs +++ b/Editor/MeshHygieneUtility.cs @@ -7,7 +7,7 @@ using UnityEngine; using UnityEditor; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { /// /// RAII-style Undo group scope. Opens an undo group on construction, collapses diff --git a/Editor/MeshOptimizer.cs b/Editor/MeshOptimizer.cs index a55ccfc1..cee83617 100644 --- a/Editor/MeshOptimizer.cs +++ b/Editor/MeshOptimizer.cs @@ -8,7 +8,7 @@ using UnityEngine; using UnityEngine.Rendering; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { public static class MeshOptimizer { diff --git a/Editor/MeshSimplifier.cs b/Editor/MeshSimplifier.cs index 33b71776..95211e56 100644 --- a/Editor/MeshSimplifier.cs +++ b/Editor/MeshSimplifier.cs @@ -6,7 +6,7 @@ using UnityEngine; using UnityEngine.Rendering; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { public static class MeshSimplifier { diff --git a/Editor/MeshoptNative.cs b/Editor/MeshoptNative.cs index 9be509e8..1fe770ea 100644 --- a/Editor/MeshoptNative.cs +++ b/Editor/MeshoptNative.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { public static class MeshoptNative { diff --git a/Editor/ModelBuilderPreview.cs b/Editor/ModelBuilderPreview.cs index 416e5730..12e9b3e4 100644 --- a/Editor/ModelBuilderPreview.cs +++ b/Editor/ModelBuilderPreview.cs @@ -9,7 +9,7 @@ using UnityEngine.Rendering; using UnityEditor; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { internal class ModelBuilderPreview { diff --git a/Editor/PostprocessorDefineManager.cs b/Editor/PostprocessorDefineManager.cs index 9ffca89f..2a47462b 100644 --- a/Editor/PostprocessorDefineManager.cs +++ b/Editor/PostprocessorDefineManager.cs @@ -2,7 +2,7 @@ // Storage now lives in MeshLabProjectSettings (per-project, committed). // This class is kept as a thin shim so existing call sites don't need changes. -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { static class PostprocessorDefineManager { diff --git a/Editor/Settings/MeshLabProjectSettings.cs b/Editor/Settings/MeshLabProjectSettings.cs index a43c569d..fb71dd09 100644 --- a/Editor/Settings/MeshLabProjectSettings.cs +++ b/Editor/Settings/MeshLabProjectSettings.cs @@ -2,7 +2,7 @@ using UnityEditor; using UnityEngine; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { class MeshLabProjectSettings : ScriptableObject { diff --git a/Editor/ShellColorModelPreview.cs b/Editor/ShellColorModelPreview.cs index 0e88918e..d4e5927f 100644 --- a/Editor/ShellColorModelPreview.cs +++ b/Editor/ShellColorModelPreview.cs @@ -2,7 +2,7 @@ using UnityEditor; using UnityEngine; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { public static class ShellColorModelPreview { diff --git a/Editor/SpatialPartitioner.cs b/Editor/SpatialPartitioner.cs index 3bec1f23..ebaf2438 100644 --- a/Editor/SpatialPartitioner.cs +++ b/Editor/SpatialPartitioner.cs @@ -20,7 +20,7 @@ using System.Collections.Generic; using UnityEngine; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { public static class SpatialPartitioner { diff --git a/Editor/StripParameterization.cs b/Editor/StripParameterization.cs index 51d0cf49..2cc740a8 100644 --- a/Editor/StripParameterization.cs +++ b/Editor/StripParameterization.cs @@ -7,7 +7,7 @@ using System.Collections.Generic; using UnityEngine; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { public static class StripParameterization { diff --git a/Editor/SymmetrySplitShells.cs b/Editor/SymmetrySplitShells.cs index dae64f7b..2307da6d 100644 --- a/Editor/SymmetrySplitShells.cs +++ b/Editor/SymmetrySplitShells.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using UnityEngine; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { public static class SymmetrySplitShells { diff --git a/Editor/Tools/AtlasPackTool.cs b/Editor/Tools/AtlasPackTool.cs index ebcc096c..83dc10c8 100644 --- a/Editor/Tools/AtlasPackTool.cs +++ b/Editor/Tools/AtlasPackTool.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using UnityEditor; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { public class AtlasPackTool : IUvTool { diff --git a/Editor/Tools/CleanupTool.cs b/Editor/Tools/CleanupTool.cs index daa556b2..28cf33de 100644 --- a/Editor/Tools/CleanupTool.cs +++ b/Editor/Tools/CleanupTool.cs @@ -7,7 +7,7 @@ using UnityEngine; using UnityEditor; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { public class CleanupTool : IUvTool { diff --git a/Editor/Tools/CollisionMeshTool.cs b/Editor/Tools/CollisionMeshTool.cs index f6ed5030..7a879461 100644 --- a/Editor/Tools/CollisionMeshTool.cs +++ b/Editor/Tools/CollisionMeshTool.cs @@ -8,7 +8,7 @@ using UnityEngine; using UnityEditor; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { public class CollisionMeshTool : IUvTool { diff --git a/Editor/Tools/LightmapTransferTool.cs b/Editor/Tools/LightmapTransferTool.cs index b370c407..387c58bf 100644 --- a/Editor/Tools/LightmapTransferTool.cs +++ b/Editor/Tools/LightmapTransferTool.cs @@ -10,7 +10,7 @@ using UnityEditor.Formats.Fbx.Exporter; #endif -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { public class LightmapTransferTool : IUvTool { diff --git a/Editor/Tools/LodGenerationTool.cs b/Editor/Tools/LodGenerationTool.cs index 2f90981a..7c601b80 100644 --- a/Editor/Tools/LodGenerationTool.cs +++ b/Editor/Tools/LodGenerationTool.cs @@ -11,7 +11,7 @@ using UnityEngine.Rendering; using UnityEditor; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { public class LodGenerationTool : IUvTool { diff --git a/Editor/Tools/ModelBuilderTool.cs b/Editor/Tools/ModelBuilderTool.cs index 07d33cd2..20ec5ebd 100644 --- a/Editor/Tools/ModelBuilderTool.cs +++ b/Editor/Tools/ModelBuilderTool.cs @@ -6,7 +6,7 @@ using UnityEngine; using UnityEditor; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { public class ModelBuilderTool : IUvTool { diff --git a/Editor/Tools/Uv0OptimizeTool.cs b/Editor/Tools/Uv0OptimizeTool.cs index b7e4eec5..02b68e39 100644 --- a/Editor/Tools/Uv0OptimizeTool.cs +++ b/Editor/Tools/Uv0OptimizeTool.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using UnityEditor; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { public class Uv0OptimizeTool : IUvTool { diff --git a/Editor/Tools/VertexAOTool.cs b/Editor/Tools/VertexAOTool.cs index 7d03c2bc..0b96c4e9 100644 --- a/Editor/Tools/VertexAOTool.cs +++ b/Editor/Tools/VertexAOTool.cs @@ -10,7 +10,7 @@ using UnityEngine.Rendering; using UnityEditor; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { public class VertexAOTool : IUvTool { diff --git a/Editor/TransferData.cs b/Editor/TransferData.cs index ff62ec86..9597df78 100644 --- a/Editor/TransferData.cs +++ b/Editor/TransferData.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using UnityEngine; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { // ─── Triangle status after transfer ─── public enum TriangleStatus diff --git a/Editor/TransferValidator.cs b/Editor/TransferValidator.cs index 06a0cdbc..650ca0d3 100644 --- a/Editor/TransferValidator.cs +++ b/Editor/TransferValidator.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using UnityEngine; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { public static class TransferValidator { diff --git a/Editor/TriangleBvh.cs b/Editor/TriangleBvh.cs index 25ae6124..04a35013 100644 --- a/Editor/TriangleBvh.cs +++ b/Editor/TriangleBvh.cs @@ -3,7 +3,7 @@ using UnityEngine; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { public class TriangleBvh { diff --git a/Editor/TriangleBvh2D.cs b/Editor/TriangleBvh2D.cs index 7186b5ba..21c0e014 100644 --- a/Editor/TriangleBvh2D.cs +++ b/Editor/TriangleBvh2D.cs @@ -5,7 +5,7 @@ using UnityEngine; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { public class TriangleBvh2D { diff --git a/Editor/Uv0Analyzer.cs b/Editor/Uv0Analyzer.cs index de6e4f77..7c6cbde2 100644 --- a/Editor/Uv0Analyzer.cs +++ b/Editor/Uv0Analyzer.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using UnityEngine; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { /// Analysis report for one mesh's UV0 quality. public struct Uv0Report diff --git a/Editor/Uv2AssetPostprocessor.cs b/Editor/Uv2AssetPostprocessor.cs index f8f5b2d4..b1fc4e28 100644 --- a/Editor/Uv2AssetPostprocessor.cs +++ b/Editor/Uv2AssetPostprocessor.cs @@ -8,7 +8,7 @@ using UnityEngine; using UnityEditor; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { public class Uv2AssetPostprocessor : AssetPostprocessor { diff --git a/Editor/Uv2DataAsset.cs b/Editor/Uv2DataAsset.cs index 1f0cd190..dab5eb79 100644 --- a/Editor/Uv2DataAsset.cs +++ b/Editor/Uv2DataAsset.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using UnityEngine; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { /// /// Fingerprint of a raw FBX mesh at the time the sidecar was created. diff --git a/Editor/UvMetricCalculator.cs b/Editor/UvMetricCalculator.cs index ad4d8621..849ac249 100644 --- a/Editor/UvMetricCalculator.cs +++ b/Editor/UvMetricCalculator.cs @@ -3,7 +3,7 @@ using UnityEngine; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { public static class UvMetricCalculator { diff --git a/Editor/UvShellExtractor.cs b/Editor/UvShellExtractor.cs index a6adfa28..7cecbf11 100644 --- a/Editor/UvShellExtractor.cs +++ b/Editor/UvShellExtractor.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using UnityEngine; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { /// /// Geometry-based descriptor for a UV shell. Computed from UV0 properties diff --git a/Editor/UvTransferPipeline.cs b/Editor/UvTransferPipeline.cs index 7c0a6d6f..d374d22f 100644 --- a/Editor/UvTransferPipeline.cs +++ b/Editor/UvTransferPipeline.cs @@ -1,7 +1,7 @@ // UvTransferPipeline.cs — Pipeline settings for UV transfer // The actual transfer is handled by GroupedShellTransfer.Transfer(). -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { public static class UvTransferPipeline { diff --git a/Editor/UvtLog.cs b/Editor/UvtLog.cs index 083edb8b..fb3661c7 100644 --- a/Editor/UvtLog.cs +++ b/Editor/UvtLog.cs @@ -1,7 +1,7 @@ using UnityEngine; using UnityEditor; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { /// /// Centralized logger with verbosity levels. diff --git a/Editor/VertexAOBaker.Blur.cs b/Editor/VertexAOBaker.Blur.cs index 7b4671a3..631fca79 100644 --- a/Editor/VertexAOBaker.Blur.cs +++ b/Editor/VertexAOBaker.Blur.cs @@ -2,7 +2,7 @@ using System.Threading.Tasks; using UnityEngine; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { public static partial class VertexAOBaker { diff --git a/Editor/VertexAOBaker.Cpu.cs b/Editor/VertexAOBaker.Cpu.cs index 7c3b3e05..07d9d594 100644 --- a/Editor/VertexAOBaker.Cpu.cs +++ b/Editor/VertexAOBaker.Cpu.cs @@ -4,7 +4,7 @@ using UnityEngine; using UnityEditor; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { public static partial class VertexAOBaker { diff --git a/Editor/VertexAOBaker.Gpu.cs b/Editor/VertexAOBaker.Gpu.cs index 4fac9e74..b641bbb4 100644 --- a/Editor/VertexAOBaker.Gpu.cs +++ b/Editor/VertexAOBaker.Gpu.cs @@ -4,7 +4,7 @@ using UnityEngine.Rendering; using UnityEditor; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { public static partial class VertexAOBaker { diff --git a/Editor/VertexAOBaker.cs b/Editor/VertexAOBaker.cs index b2b2447d..5fc2a0f6 100644 --- a/Editor/VertexAOBaker.cs +++ b/Editor/VertexAOBaker.cs @@ -8,7 +8,7 @@ using UnityEngine; using UnityEditor; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { public enum AOTargetChannel { diff --git a/Editor/XatlasNative.cs b/Editor/XatlasNative.cs index 33c5b280..f1b77a53 100644 --- a/Editor/XatlasNative.cs +++ b/Editor/XatlasNative.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { public static class XatlasNative { diff --git a/Editor/XatlasRepack.cs b/Editor/XatlasRepack.cs index 3667795d..e06cfa3f 100644 --- a/Editor/XatlasRepack.cs +++ b/Editor/XatlasRepack.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using UnityEngine; -namespace LightmapUvTool +namespace SashaRX.UnityMeshLab { public struct RepackOptions { From 80db744e24d88f637c6269ef39fb1cb4e15d228a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Apr 2026 07:50:59 +0000 Subject: [PATCH 09/21] refactor(asmdef): update name and rootNamespace to SashaRX.UnityMeshLab.Editor Asmdef file kept at its current path (LightmapUvTool.Editor.asmdef) to preserve its GUID; file rename is a separate commit. --- Editor/LightmapUvTool.Editor.asmdef | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Editor/LightmapUvTool.Editor.asmdef b/Editor/LightmapUvTool.Editor.asmdef index c2cfffe2..990278d3 100644 --- a/Editor/LightmapUvTool.Editor.asmdef +++ b/Editor/LightmapUvTool.Editor.asmdef @@ -1,6 +1,6 @@ { - "name": "LightmapUvTool.Editor", - "rootNamespace": "LightmapUvTool", + "name": "SashaRX.UnityMeshLab.Editor", + "rootNamespace": "SashaRX.UnityMeshLab", "references": [ "Unity.Formats.Fbx.Editor" ], From ae6cf95682b31a70104bb14fde3f91d652eaffc8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Apr 2026 07:51:12 +0000 Subject: [PATCH 10/21] refactor(asmdef): rename LightmapUvTool.Editor.asmdef -> SashaRX.UnityMeshLab.Editor.asmdef Renames the asmdef file (and its .meta sibling) to match the already-updated name field. GUID 832f52d32c124f8895ff1ed7c0c050f5 is preserved. --- ...mapUvTool.Editor.asmdef => SashaRX.UnityMeshLab.Editor.asmdef} | 0 ...Editor.asmdef.meta => SashaRX.UnityMeshLab.Editor.asmdef.meta} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename Editor/{LightmapUvTool.Editor.asmdef => SashaRX.UnityMeshLab.Editor.asmdef} (100%) rename Editor/{LightmapUvTool.Editor.asmdef.meta => SashaRX.UnityMeshLab.Editor.asmdef.meta} (100%) diff --git a/Editor/LightmapUvTool.Editor.asmdef b/Editor/SashaRX.UnityMeshLab.Editor.asmdef similarity index 100% rename from Editor/LightmapUvTool.Editor.asmdef rename to Editor/SashaRX.UnityMeshLab.Editor.asmdef diff --git a/Editor/LightmapUvTool.Editor.asmdef.meta b/Editor/SashaRX.UnityMeshLab.Editor.asmdef.meta similarity index 100% rename from Editor/LightmapUvTool.Editor.asmdef.meta rename to Editor/SashaRX.UnityMeshLab.Editor.asmdef.meta From 7e7eab9e7f0023a0015ced0c743f34b6dd66a385 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Apr 2026 07:51:59 +0000 Subject: [PATCH 11/21] docs: update AGENTS/CLAUDE/repo-conventions for SashaRX.UnityMeshLab namespace --- .claude/skills/repo-conventions/SKILL.md | 4 ++-- AGENTS.md | 4 ++-- CLAUDE.md | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.claude/skills/repo-conventions/SKILL.md b/.claude/skills/repo-conventions/SKILL.md index a452033f..6d6d29af 100644 --- a/.claude/skills/repo-conventions/SKILL.md +++ b/.claude/skills/repo-conventions/SKILL.md @@ -44,7 +44,7 @@ Missing and planned (see `unity-ci-validation/SKILL.md`): Both items below are known deviations captured for tracking; the migration to the canonical values is planned (see "Migration status" below). - **`package.json` `name`** is currently `com.sasharx.lightmap-uv-tool` — does NOT match the repository folder name `UnityMeshLab`. Canonical value: `com.sasharx.unitymeshlab`. Rationale for deviation: historical — the repo originated as `lightmap-uv-tool` before the mesh-lab rename. Migration breaks downstream consumers; schedule under a MAJOR SemVer bump. -- **Root namespace** is currently `LightmapUvTool` (bare single-segment) in `.cs` files under `Editor/`. Canonical value: `SashaRX.UnityMeshLab`. Rationale for deviation: none; pre-dates the two-segment rule. Migration tracked via `migration-and-refactor-planner`. +- *(Resolved in the `claude/skills-overhaul-phase-0-Xrg7K` branch: namespace migrated from bare `LightmapUvTool` to `SashaRX.UnityMeshLab` across 48 `.cs` files; asmdef renamed `LightmapUvTool.Editor.asmdef` → `SashaRX.UnityMeshLab.Editor.asmdef`, GUID preserved.)* ## Primary domain vocabulary @@ -68,7 +68,7 @@ Shared with agents via `CLAUDE.md`: ## Migration status -Tracked rename of `LightmapUvTool` → `SashaRX.UnityMeshLab` is scheduled under `migration-and-refactor-planner`. Until complete, new code in `Editor/` and `Runtime/` must still use the current bare namespace to avoid mixing conventions mid-migration. After the migration lands, this section will be removed and the Deviations section updated. +Namespace migration to `SashaRX.UnityMeshLab` is complete. The `package.json` `name` rename from `com.sasharx.lightmap-uv-tool` to `com.sasharx.unitymeshlab` remains scheduled — it is a downstream-breaking change and must ship under a MAJOR SemVer bump with explicit user communication. ## Further reading diff --git a/AGENTS.md b/AGENTS.md index bd46ae1a..350f4764 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,7 +23,7 @@ Shared rules for **all AI agents** (Codex, Claude, etc.) working on this reposit - Do NOT commit bulk `.meta` changes unless files were actually added/removed ### Assembly & platform -- All Editor code under `Editor/` with `LightmapUvTool.Editor.asmdef` +- All Editor code under `Editor/` with `SashaRX.UnityMeshLab.Editor.asmdef` - `includePlatforms: ["Editor"]` — never leak into runtime builds - Do NOT mix Runtime and Editor dependencies - FBX exporter code gated by `#if LIGHTMAP_UV_TOOL_FBX_EXPORTER` @@ -40,7 +40,7 @@ Shared rules for **all AI agents** (Codex, Claude, etc.) working on this reposit - Document result in `EXPERIMENTS.md` before merging ### Code conventions -- Namespace: `LightmapUvTool` +- Namespace: `SashaRX.UnityMeshLab` - No `using System.Text.RegularExpressions` in `LightmapTransferTool.cs` — use fully qualified path - `internal` visibility for cross-tool helpers (same assembly) - All scene modifications via `Undo.RecordObject` / `Undo.AddComponent` / `Undo.DestroyObjectImmediate` diff --git a/CLAUDE.md b/CLAUDE.md index a7c8153f..6214243c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,7 +17,7 @@ See `AGENTS.md` for shared rules that apply to all AI agents. ## Code Rules -- Namespace: `LightmapUvTool` +- Namespace: `SashaRX.UnityMeshLab` - No `using System.Text.RegularExpressions` in `LightmapTransferTool.cs` — use fully qualified `System.Text.RegularExpressions.Regex` - `internal` visibility for cross-tool helpers (same assembly) - `Undo.RecordObject` / `Undo.AddComponent` / `Undo.DestroyObjectImmediate` for all scene modifications From 1fa9ed3bf0a8e296eb8f351629e15c84f0fbdfea Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Apr 2026 21:24:56 +0000 Subject: [PATCH 12/21] feat(skills): populate _template/package-template/ with canonical placeholders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Normalize placeholder scheme to {{PackageName}}, {{Namespace}}, {{PackageId}}, {{DisplayName}}, {{Description}}, {{UnityMinVersion}}, {{Author}}, {{License}}, {{Year}}, {{Date}} — remove inconsistent {{Company}}, {{company}}, {{Display Name}}, {{repo}}, {{author}}, {{YYYY-MM-DD}}, {{YYYY}} variants. - Translate README.md.template from Russian to English. - Rename Editor/Company.PackageName.Editor.asmdef.template -> Editor/SashaRX.{{PackageName}}.Editor.asmdef.template and equivalent for Tests. - Rename LICENSE -> LICENSE.template for consistency with other template files. - Add missing files per unity-package-bootstrap/SKILL.md manifest: .gitignore.template, Runtime/SashaRX.{{PackageName}}.asmdef.template, Runtime/PackageRuntimeEntryPoint.cs.template, Editor/PackageEditorEntryPoint.cs.template, Tests/Editor/SmokeTests.cs.template, Samples~/Basic/README.md.template, Documentation~/index.md.template. - Every .asmdef template parses as valid JSON after placeholder substitution. - Update unity-package-bootstrap/SKILL.md to reflect the new files and {{Year}}/{{Date}} parameters. --- .../package-template/.gitignore.template | 29 +++++++++++++++ .../package-template/CHANGELOG.md.template | 2 +- .../Documentation~/index.md.template | 13 +++++++ .../PackageEditorEntryPoint.cs.template | 17 +++++++++ ...aRX.{{PackageName}}.Editor.asmdef.template | 14 ++++++++ .../{LICENSE => LICENSE.template} | 2 +- .../package-template/README.md.template | 36 +++++++++---------- .../PackageRuntimeEntryPoint.cs.template | 8 +++++ .../SashaRX.{{PackageName}}.asmdef.template} | 6 ++-- .../Samples~/Basic/README.md.template | 9 +++++ ...ackageName}}.Tests.Editor.asmdef.template} | 12 +++++-- .../Tests/Editor/SmokeTests.cs.template | 14 ++++++++ .../package-template/package.json.template | 27 +++++++++----- .../skills/unity-package-bootstrap/SKILL.md | 5 ++- 14 files changed, 158 insertions(+), 36 deletions(-) create mode 100644 .claude/skills/_template/package-template/.gitignore.template create mode 100644 .claude/skills/_template/package-template/Documentation~/index.md.template create mode 100644 .claude/skills/_template/package-template/Editor/PackageEditorEntryPoint.cs.template create mode 100644 .claude/skills/_template/package-template/Editor/SashaRX.{{PackageName}}.Editor.asmdef.template rename .claude/skills/_template/package-template/{LICENSE => LICENSE.template} (96%) create mode 100644 .claude/skills/_template/package-template/Runtime/PackageRuntimeEntryPoint.cs.template rename .claude/skills/_template/package-template/{Editor/Company.PackageName.Editor.asmdef.template => Runtime/SashaRX.{{PackageName}}.asmdef.template} (65%) create mode 100644 .claude/skills/_template/package-template/Samples~/Basic/README.md.template rename .claude/skills/_template/package-template/Tests/Editor/{Company.PackageName.Tests.Editor.asmdef.template => SashaRX.{{PackageName}}.Tests.Editor.asmdef.template} (53%) create mode 100644 .claude/skills/_template/package-template/Tests/Editor/SmokeTests.cs.template diff --git a/.claude/skills/_template/package-template/.gitignore.template b/.claude/skills/_template/package-template/.gitignore.template new file mode 100644 index 00000000..b6f63091 --- /dev/null +++ b/.claude/skills/_template/package-template/.gitignore.template @@ -0,0 +1,29 @@ +# Unity-generated +Library/ +Logs/ +Temp/ +Obj/ +Build/ +Builds/ +UserSettings/ + +# IDE +.vs/ +.idea/ +*.csproj +*.sln +*.user +*.suo +*.pidb +*.booproj +*.svd +*.pdb + +# OS +.DS_Store +Thumbs.db + +# Sensitive +.env +*.key +*.pem diff --git a/.claude/skills/_template/package-template/CHANGELOG.md.template b/.claude/skills/_template/package-template/CHANGELOG.md.template index 09cdbce8..039b4dce 100644 --- a/.claude/skills/_template/package-template/CHANGELOG.md.template +++ b/.claude/skills/_template/package-template/CHANGELOG.md.template @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] -## [0.1.0] - {{YYYY-MM-DD}} +## [0.1.0] - {{Date}} ### Added - Initial release. diff --git a/.claude/skills/_template/package-template/Documentation~/index.md.template b/.claude/skills/_template/package-template/Documentation~/index.md.template new file mode 100644 index 00000000..a5658e88 --- /dev/null +++ b/.claude/skills/_template/package-template/Documentation~/index.md.template @@ -0,0 +1,13 @@ +# {{DisplayName}} + +{{Description}} + +## Contents + +- [Installation](../README.md#installation) +- [Usage](../README.md#usage) +- [Changelog](../CHANGELOG.md) + +## API overview + +_Document the public API surface here. Group by primary concept (e.g. windows, tools, utilities)._ diff --git a/.claude/skills/_template/package-template/Editor/PackageEditorEntryPoint.cs.template b/.claude/skills/_template/package-template/Editor/PackageEditorEntryPoint.cs.template new file mode 100644 index 00000000..c4f48488 --- /dev/null +++ b/.claude/skills/_template/package-template/Editor/PackageEditorEntryPoint.cs.template @@ -0,0 +1,17 @@ +using UnityEditor; +using UnityEngine; + +namespace {{Namespace}}.Editor +{ + internal static class PackageEditorEntryPoint + { + [MenuItem("Tools/{{DisplayName}}/About")] + private static void About() + { + EditorUtility.DisplayDialog( + PackageInfo.DisplayName, + $"{PackageInfo.DisplayName}\nPackage: {PackageInfo.PackageId}", + "OK"); + } + } +} diff --git a/.claude/skills/_template/package-template/Editor/SashaRX.{{PackageName}}.Editor.asmdef.template b/.claude/skills/_template/package-template/Editor/SashaRX.{{PackageName}}.Editor.asmdef.template new file mode 100644 index 00000000..776f1e37 --- /dev/null +++ b/.claude/skills/_template/package-template/Editor/SashaRX.{{PackageName}}.Editor.asmdef.template @@ -0,0 +1,14 @@ +{ + "name": "{{Namespace}}.Editor", + "rootNamespace": "{{Namespace}}.Editor", + "references": [ "{{Namespace}}" ], + "includePlatforms": ["Editor"], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": false, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/.claude/skills/_template/package-template/LICENSE b/.claude/skills/_template/package-template/LICENSE.template similarity index 96% rename from .claude/skills/_template/package-template/LICENSE rename to .claude/skills/_template/package-template/LICENSE.template index 672ae5b6..9ed2e73d 100644 --- a/.claude/skills/_template/package-template/LICENSE +++ b/.claude/skills/_template/package-template/LICENSE.template @@ -1,6 +1,6 @@ MIT License -Copyright (c) {{YYYY}} {{Author}} +Copyright (c) {{Year}} {{Author}} Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/.claude/skills/_template/package-template/README.md.template b/.claude/skills/_template/package-template/README.md.template index dd20eb61..d336730b 100644 --- a/.claude/skills/_template/package-template/README.md.template +++ b/.claude/skills/_template/package-template/README.md.template @@ -1,30 +1,30 @@ -# {{Display Name}} +# {{DisplayName}} -{{Описание пакета.}} +{{Description}} -## Установка +## Installation -### Через Git URL (Unity Package Manager) +### Via Git URL (Unity Package Manager) -1. Откройте **Window → Package Manager** -2. Нажмите **+** → **Add package from git URL** -3. Вставьте: `https://github.com/{{author}}/{{repo}}.git` +1. Open **Window → Package Manager**. +2. Click **+** → **Add package from git URL**. +3. Paste: `https://github.com/{{Author}}/{{PackageName}}.git`. -### Через диск +### Via local disk -1. Скачайте или клонируйте репозиторий -2. Откройте **Window → Package Manager** -3. Нажмите **+** → **Add package from disk** -4. Выберите файл `package.json` +1. Clone this repository. +2. Open **Window → Package Manager**. +3. Click **+** → **Add package from disk**. +4. Select the `package.json` at the repository root. -## Использование +## Usage -_Описать основной workflow._ +_Describe the primary workflow here._ -## Требования +## Requirements -- Unity {{unity_version}} или новее +- Unity {{UnityMinVersion}} or newer. -## Лицензия +## License -MIT — см. [LICENSE](LICENSE) +{{License}} — see [LICENSE](LICENSE). diff --git a/.claude/skills/_template/package-template/Runtime/PackageRuntimeEntryPoint.cs.template b/.claude/skills/_template/package-template/Runtime/PackageRuntimeEntryPoint.cs.template new file mode 100644 index 00000000..0109e37c --- /dev/null +++ b/.claude/skills/_template/package-template/Runtime/PackageRuntimeEntryPoint.cs.template @@ -0,0 +1,8 @@ +namespace {{Namespace}} +{ + public static class PackageInfo + { + public const string PackageId = "{{PackageId}}"; + public const string DisplayName = "{{DisplayName}}"; + } +} diff --git a/.claude/skills/_template/package-template/Editor/Company.PackageName.Editor.asmdef.template b/.claude/skills/_template/package-template/Runtime/SashaRX.{{PackageName}}.asmdef.template similarity index 65% rename from .claude/skills/_template/package-template/Editor/Company.PackageName.Editor.asmdef.template rename to .claude/skills/_template/package-template/Runtime/SashaRX.{{PackageName}}.asmdef.template index 8f5a34f6..571d4baa 100644 --- a/.claude/skills/_template/package-template/Editor/Company.PackageName.Editor.asmdef.template +++ b/.claude/skills/_template/package-template/Runtime/SashaRX.{{PackageName}}.asmdef.template @@ -1,8 +1,8 @@ { - "name": "{{Company}}.{{PackageName}}.Editor", - "rootNamespace": "{{Company}}.{{PackageName}}", + "name": "{{Namespace}}", + "rootNamespace": "{{Namespace}}", "references": [], - "includePlatforms": ["Editor"], + "includePlatforms": [], "excludePlatforms": [], "allowUnsafeCode": false, "overrideReferences": false, diff --git a/.claude/skills/_template/package-template/Samples~/Basic/README.md.template b/.claude/skills/_template/package-template/Samples~/Basic/README.md.template new file mode 100644 index 00000000..3a8296b8 --- /dev/null +++ b/.claude/skills/_template/package-template/Samples~/Basic/README.md.template @@ -0,0 +1,9 @@ +# Basic sample — {{DisplayName}} + +Minimum usage example. + +## How to run + +1. Import this sample via **Window → Package Manager → {{DisplayName}} → Samples → Basic**. +2. Open the sample scene (if provided) or the generated assets under `Assets/Samples/{{DisplayName}}//Basic`. +3. Follow the in-scene instructions. diff --git a/.claude/skills/_template/package-template/Tests/Editor/Company.PackageName.Tests.Editor.asmdef.template b/.claude/skills/_template/package-template/Tests/Editor/SashaRX.{{PackageName}}.Tests.Editor.asmdef.template similarity index 53% rename from .claude/skills/_template/package-template/Tests/Editor/Company.PackageName.Tests.Editor.asmdef.template rename to .claude/skills/_template/package-template/Tests/Editor/SashaRX.{{PackageName}}.Tests.Editor.asmdef.template index 6debba0f..0e8f72a8 100644 --- a/.claude/skills/_template/package-template/Tests/Editor/Company.PackageName.Tests.Editor.asmdef.template +++ b/.claude/skills/_template/package-template/Tests/Editor/SashaRX.{{PackageName}}.Tests.Editor.asmdef.template @@ -1,7 +1,12 @@ { - "name": "{{Company}}.{{PackageName}}.Tests.Editor", - "rootNamespace": "{{Company}}.{{PackageName}}.Tests", - "references": ["{{Company}}.{{PackageName}}.Editor"], + "name": "{{Namespace}}.Tests.Editor", + "rootNamespace": "{{Namespace}}.Tests", + "references": [ + "{{Namespace}}", + "{{Namespace}}.Editor", + "UnityEngine.TestRunner", + "UnityEditor.TestRunner" + ], "includePlatforms": ["Editor"], "excludePlatforms": [], "allowUnsafeCode": false, @@ -9,6 +14,7 @@ "precompiledReferences": ["nunit.framework.dll"], "autoReferenced": false, "defineConstraints": ["UNITY_INCLUDE_TESTS"], + "optionalUnityReferences": ["TestAssemblies"], "versionDefines": [], "noEngineReferences": false } diff --git a/.claude/skills/_template/package-template/Tests/Editor/SmokeTests.cs.template b/.claude/skills/_template/package-template/Tests/Editor/SmokeTests.cs.template new file mode 100644 index 00000000..4355f111 --- /dev/null +++ b/.claude/skills/_template/package-template/Tests/Editor/SmokeTests.cs.template @@ -0,0 +1,14 @@ +using NUnit.Framework; + +namespace {{Namespace}}.Tests +{ + public class SmokeTests + { + [Test] + public void PackageInfo_Has_NonEmpty_Identity() + { + Assert.That(PackageInfo.PackageId, Is.Not.Null.And.Not.Empty); + Assert.That(PackageInfo.DisplayName, Is.Not.Null.And.Not.Empty); + } + } +} diff --git a/.claude/skills/_template/package-template/package.json.template b/.claude/skills/_template/package-template/package.json.template index 9b48de13..95bfc3e6 100644 --- a/.claude/skills/_template/package-template/package.json.template +++ b/.claude/skills/_template/package-template/package.json.template @@ -1,18 +1,27 @@ { - "name": "com.{{company}}.{{package-name}}", + "name": "{{PackageId}}", "version": "0.1.0", - "displayName": "{{Display Name}}", - "description": "{{Описание пакета в одно предложение.}}", - "unity": "2021.3", + "displayName": "{{DisplayName}}", + "description": "{{Description}}", + "unity": "{{UnityMinVersion}}", "author": { "name": "{{Author}}", - "url": "https://github.com/{{author}}" + "url": "https://github.com/{{Author}}" }, "repository": { "type": "git", - "url": "https://github.com/{{author}}/{{repo}}.git" + "url": "https://github.com/{{Author}}/{{PackageName}}.git" }, - "keywords": [], - "license": "MIT", - "dependencies": {} + "documentationUrl": "https://github.com/{{Author}}/{{PackageName}}", + "changelogUrl": "https://github.com/{{Author}}/{{PackageName}}/blob/main/CHANGELOG.md", + "licensesUrl": "https://github.com/{{Author}}/{{PackageName}}/blob/main/LICENSE", + "license": "{{License}}", + "dependencies": {}, + "samples": [ + { + "displayName": "Basic", + "description": "Minimum usage example", + "path": "Samples~/Basic" + } + ] } diff --git a/.claude/skills/unity-package-bootstrap/SKILL.md b/.claude/skills/unity-package-bootstrap/SKILL.md index 64eb0bf7..65ee177e 100644 --- a/.claude/skills/unity-package-bootstrap/SKILL.md +++ b/.claude/skills/unity-package-bootstrap/SKILL.md @@ -41,7 +41,8 @@ _template/package-template/ │ ├── SashaRX.{{PackageName}}.asmdef.template │ └── PackageRuntimeEntryPoint.cs.template ├── Tests/Editor/ -│ └── SashaRX.{{PackageName}}.Tests.Editor.asmdef.template +│ ├── SashaRX.{{PackageName}}.Tests.Editor.asmdef.template +│ └── SmokeTests.cs.template ├── Samples~/ │ └── Basic/ │ └── README.md.template @@ -65,6 +66,8 @@ Collect from the invoking user, with defaults: | `UnityMinVersion` | `2021.3` | `2022.3` or `6000.0` | | `Author` | `SashaRX` | | | `License` | `MIT` | | +| `Year` | current year | `2026` (for LICENSE) | +| `Date` | today | `2026-04-20` (for CHANGELOG) | Reject `PackageName` values that are not PascalCase, contain spaces, or equal a C# reserved word. From 32abbc4154bb90e6d3f544e024b2cdd92f2ee1a1 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Apr 2026 21:25:19 +0000 Subject: [PATCH 13/21] chore: add .prettierignore to protect skill YAML frontmatter Prettier reflows single-line YAML 'description:' values into folded scalars, which the Claude Code skill loader then fails to parse. Excluding .claude/skills/** from Prettier is the canonical mitigation (see .claude/skills/_shared/anti-patterns.md item 26). --- .prettierignore | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .prettierignore diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..ab24ad67 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,9 @@ +# Claude Code skills +# Prettier reflows single-line YAML descriptions in frontmatter into folded +# scalars, which the skill-loader parser then rejects — see +# .claude/skills/_shared/anti-patterns.md item 26. Keep skill authoring +# entirely out of Prettier's hands. +.claude/skills/**/SKILL.md +.claude/skills/_shared/** +.claude/skills/_checklists/** +.claude/skills/_template/** From ce2e35174bff4d845f0ec69037fefd3402d73454 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Apr 2026 22:47:04 +0000 Subject: [PATCH 14/21] docs(agents): shrink AGENTS.md to skills-directory pointer Remove duplicated skill content (meta files, assembly platform, code conventions, naming conventions) that now lives canonically in .claude/skills/_shared/ and .claude/skills/repo-conventions/SKILL.md. Keep only: - Entry-point pointers to the skill catalog. - Domain-specific rules that are not covered by any skill (native plugins, transfer-pipeline experiments, LODGroup lifecycle, sidecar assets, LOD/COL naming, FBX exporter gating). - Review focus list scoped to this repo's domain. Title bumped from 'com.sasharx.lightmap-uv-tool' to 'UnityMeshLab' to match repository identity; the package.json rename follows under a separate MAJOR-bump commit. --- AGENTS.md | 89 ++++++++++++++++++++++--------------------------------- 1 file changed, 35 insertions(+), 54 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 350f4764..3f41d57d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,69 +1,50 @@ -# AI Agent Rules — Unity Package (com.sasharx.lightmap-uv-tool) +# AI Agent Rules — UnityMeshLab -Shared rules for **all AI agents** (Codex, Claude, etc.) working on this repository. +Shared rules for all AI agents (Codex, Claude, etc.) working on this repository. -## Package Structure & Zones +The canonical rule set lives in `.claude/skills/`. This file is the top-level pointer: start here, then follow skill references. Per-repo deviations and identity (package ID, namespace, Unity minimum, CI workflows) are documented in `.claude/skills/repo-conventions/SKILL.md`. + +## Entry points + +- **Repo identity, namespaces, CI workflows** — `.claude/skills/repo-conventions/SKILL.md`. +- **Cross-repo conventions (package layout, naming, version gates, anti-patterns)** — `.claude/skills/_shared/`. +- **Verification checklists** (undo, prefab, batch, release) — `.claude/skills/_checklists/`. +- **Skill catalog** — every skill in `.claude/skills/*/SKILL.md` has scope, delegations, and canonical patterns. + +## Package-specific zones | Zone | Purpose | Sensitivity | |------|---------|-------------| -| `Editor/` | Editor-only tools (never ships in builds) | High — asmdef, Undo, lifecycle | -| `Plugins/` | Native binaries (xatlas, V-HACD, meshoptimizer) | Critical — binary must match `Native/` source | +| `Editor/` | Editor-only tools | High — asmdef, Undo, lifecycle | +| `Plugins/` | Native binaries (xatlas, V-HACD, meshoptimizer) | Critical — must match `Native/` source | | `Native/` | C/C++ source for native plugins | Critical — changes trigger CI rebuild | | `Shaders/` | Compute/render shaders for GPU tools | Medium — platform compatibility | | `package.json` | UPM manifest | High — version, dependencies | -| `CHANGELOG.md` | Release notes | Low — documentation | -| `README.md` | User-facing docs | Low — documentation | - -## Hard Rules - -### Meta files -- Every file and directory MUST have a `.meta` file -- NEVER delete, regenerate, or modify GUIDs in `.meta` files -- Do NOT create `.meta` manually — Unity generates them -- Do NOT commit bulk `.meta` changes unless files were actually added/removed - -### Assembly & platform -- All Editor code under `Editor/` with `SashaRX.UnityMeshLab.Editor.asmdef` -- `includePlatforms: ["Editor"]` — never leak into runtime builds -- Do NOT mix Runtime and Editor dependencies -- FBX exporter code gated by `#if LIGHTMAP_UV_TOOL_FBX_EXPORTER` - -### Package integrity -- Do NOT change `package.json` name/displayName without explicit request -- Do NOT break public API without clear justification and changelog entry -- Do NOT modify native binaries in `Plugins/` directly — rebuild from `Native/` source -- Define symbols (`versionDefines`) must match actual package dependencies -### Transfer Pipeline -- Before modifying GroupedShellTransfer, XatlasRepack, or SymmetrySplitShells — read `EXPERIMENTS.md` -- Each experiment = 1 small PR, 1 concern, testable on simple model first -- Document result in `EXPERIMENTS.md` before merging +## Domain-specific hard rules (not covered by skills) -### Code conventions -- Namespace: `SashaRX.UnityMeshLab` -- No `using System.Text.RegularExpressions` in `LightmapTransferTool.cs` — use fully qualified path -- `internal` visibility for cross-tool helpers (same assembly) -- All scene modifications via `Undo.RecordObject` / `Undo.AddComponent` / `Undo.DestroyObjectImmediate` -- Logging via `UvtLog.Info()` / `UvtLog.Warn()` / `UvtLog.Error()` +- **Native plugins**: never modify `Plugins/*.dll|*.so|*.dylib|*.bundle` directly — rebuild from `Native/` source via the `build-native.yml` CI workflow. +- **Transfer pipeline experiments**: read `EXPERIMENTS.md` before modifying `GroupedShellTransfer`, `XatlasRepack`, or `SymmetrySplitShells`. One experiment per PR, documented in `EXPERIMENTS.md`. +- **LODGroup lifecycle**: call `RestoreWorkingMeshes()` before clearing or switching LODGroup context. +- **LOD / collision naming**: `Name_LOD{N}` (e.g., `Chair_LOD0`), `Name_COL` or `Name_COL_Hull{N}`. Group key extracted via `UvToolContext.ExtractGroupKey()`. +- **Sidecar assets**: `ModelName_uv2data.asset` — persists UV2/collision data alongside FBX. +- **FBX exporter**: code gated by `#if LIGHTMAP_UV_TOOL_FBX_EXPORTER`. +- **Regex in `LightmapTransferTool.cs`**: use fully-qualified `System.Text.RegularExpressions.Regex` — no top-level `using`. +- **Logging**: `UvtLog.Info` / `UvtLog.Warn` / `UvtLog.Error` (prefix `[LightmapUV]`). -## Review Focus (Critical Issues) +For mutation safety, package structure, serialization, CI, and release mechanics — consult the relevant skill in `.claude/skills/`, not this file. -For review, these are the **actually important** things to catch in this package: +## Review focus -1. **API breaks** — public method signature changes, removed types, renamed serialized fields -2. **GC spikes** — allocations in `OnGUI`, `Update`, `OnSceneGUI` hot paths -3. **Editor/Runtime leakage** — runtime code referencing `UnityEditor`, or editor code missing platform guards -4. **Serialization issues** — changed `[Serializable]` field types/names break existing sidecar assets -5. **Domain reload** — static state that survives assembly reload without cleanup -6. **asmdef dependencies** — missing references, circular deps, wrong platform filters -7. **Mesh/Object lifecycle** — temporary meshes not destroyed, MeshFilter.sharedMesh not restored -8. **LODGroup lifecycle** — `RestoreWorkingMeshes()` before clearing/switching context -9. **Native plugin ABI** — C# marshalling must match C++ signatures exactly -10. **Undo support** — all scene modifications must be undoable +For review, the actually-important things to catch in this package: -## Naming Conventions +1. **API breaks** — public method signatures, removed types, renamed serialized fields. +2. **Editor/Runtime leakage** — runtime code referencing `UnityEditor`; editor code missing platform guards. +3. **Serialization** — changed `[Serializable]` field types/names break existing sidecar assets. +4. **Domain reload** — static state that survives assembly reload without cleanup. +5. **Native plugin ABI** — C# marshalling must match C++ signatures exactly. +6. **LODGroup lifecycle** — `RestoreWorkingMeshes()` ordering. +7. **Mesh lifecycle** — temporary meshes destroyed, `MeshFilter.sharedMesh` restored. +8. **Undo coverage** — scene modifications must be undoable (see `unity-undo-prefab-safety`). -- LOD objects: `Name_LOD{N}` (e.g., `Chair_LOD0`, `Chair_LOD1`) -- Collision objects: `Name_COL` (simplified) or `Name_COL_Hull{N}` (convex decomp) -- Mesh group key: `UvToolContext.ExtractGroupKey()` strips LOD/COL suffixes -- Sidecar assets: `ModelName_uv2data.asset` +Mutation-safety, batching, prefab, serialization, and package rules are enforced through the skills in `.claude/skills/`; the reviewer skill (`unity-package-reviewer`) and the auditor skill (`repo-auditor`) cite line-level violations. From f48573f14f2b5eb3408ae276b892ca8a2822ed16 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Apr 2026 22:48:34 +0000 Subject: [PATCH 15/21] docs(skills): directive phrasing on 3 critical skills MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per Seleznov's A/B study (~100% activation vs ~77% with passive 'Use when …'), the three highest-criticality skills get directive 'ALWAYS invoke this skill when …' phrasing plus explicit 'Do not …' prohibitions: - unity-undo-prefab-safety (every editor mutation must be undoable) - unity-assetdatabase-tools (every batch must pair Start/Stop in try/finally) - unity-package-architect (every new package follows the canonical layout) Remaining skills keep passive 'Use when …' to avoid directive saturation. --- .claude/skills/unity-assetdatabase-tools/SKILL.md | 2 +- .claude/skills/unity-package-architect/SKILL.md | 2 +- .claude/skills/unity-undo-prefab-safety/SKILL.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.claude/skills/unity-assetdatabase-tools/SKILL.md b/.claude/skills/unity-assetdatabase-tools/SKILL.md index a3c49bf4..7be6fc48 100644 --- a/.claude/skills/unity-assetdatabase-tools/SKILL.md +++ b/.claude/skills/unity-assetdatabase-tools/SKILL.md @@ -1,6 +1,6 @@ --- name: unity-assetdatabase-tools -description: Safely batch AssetDatabase operations, move and rename assets preserving GUIDs, and write AssetPostprocessors with recursion guards. Use when code touches AssetDatabase, AssetImporter, AssetPostprocessor, .meta files, or performs bulk asset creation, import, move, copy, or delete. Always wrap batches in try/finally with StartAssetEditing/StopAssetEditing, or AssetEditingScope on Unity 6+. +description: ALWAYS invoke this skill when code touches AssetDatabase, AssetImporter, AssetPostprocessor, .meta files, or performs bulk asset creation/import/move/copy/delete. Do not use File.Move or File.Delete on anything under Assets/ or Packages/; do not call AssetDatabase.Refresh inside a loop; do not write an AssetPostprocessor without a static HashSet recursion guard. Mandatory — wrap batches in try/finally with StartAssetEditing and StopAssetEditing, or AssetEditingScope on Unity 6+. paths: ["**/*AssetPostprocessor*.cs", "**/*Importer*.cs"] --- diff --git a/.claude/skills/unity-package-architect/SKILL.md b/.claude/skills/unity-package-architect/SKILL.md index 55361834..2fb6701b 100644 --- a/.claude/skills/unity-package-architect/SKILL.md +++ b/.claude/skills/unity-package-architect/SKILL.md @@ -1,6 +1,6 @@ --- name: unity-package-architect -description: Author Unity UPM package skeletons with canonical layout — package.json, asmdef boundaries, Runtime/Editor/Tests split, Samples~ and Documentation~ tilde-hidden folders. Use when creating a new UPM package, restructuring a package, editing package.json, or adding/splitting asmdefs. Namespaces follow SashaRX.; see _shared/naming-conventions.md. +description: ALWAYS invoke this skill when creating a new Unity UPM package, restructuring a package, editing package.json, adding or splitting asmdefs, or touching Runtime/Editor/Tests/Samples~/Documentation~ layout. Do not place Runtime code in an Editor asmdef or vice versa; do not use single-segment namespaces; do not ship Samples/ without the trailing tilde. Mandatory — namespaces follow SashaRX., asmdef name equals file basename, includePlatforms and excludePlatforms are mutually exclusive. paths: ["**/package.json", "**/*.asmdef", "**/*.asmref"] --- diff --git a/.claude/skills/unity-undo-prefab-safety/SKILL.md b/.claude/skills/unity-undo-prefab-safety/SKILL.md index 74beb25b..37e9c62d 100644 --- a/.claude/skills/unity-undo-prefab-safety/SKILL.md +++ b/.claude/skills/unity-undo-prefab-safety/SKILL.md @@ -1,6 +1,6 @@ --- name: unity-undo-prefab-safety -description: Make every editor mutation undoable and every prefab edit safe. Use when code modifies scene GameObjects, components, prefab assets, or prefab instance overrides, or when using Undo, PrefabUtility, PrefabStage, or EditorUtility.SetDirty. ALWAYS call Undo.RecordObject before mutation, PrefabUtility.RecordPrefabInstancePropertyModifications after instance edits, and EditPrefabContentsScope (2020.1+) for asset edits. Do not use File.* for assets — use AssetDatabase APIs (delegate to unity-assetdatabase-tools). +description: ALWAYS invoke this skill when code modifies a Unity scene GameObject, Component, prefab asset, or prefab instance override, or when using Undo, PrefabUtility, PrefabStage, or EditorUtility.SetDirty. Do not mutate target fields directly in a CustomEditor; do not edit prefab assets via LoadAssetAtPath; do not use File.* on assets. Mandatory sequence — Undo.RecordObject BEFORE mutation, PrefabUtility.RecordPrefabInstancePropertyModifications AFTER instance edits, PrefabUtility.EditPrefabContentsScope (2020.1+) for asset edits. paths: ["**/Editor/**/*.cs"] --- From f7b37e8ef697f9968526f71dc488f45b55761b32 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Apr 2026 22:49:21 +0000 Subject: [PATCH 16/21] refactor!: rename package id com.sasharx.lightmap-uv-tool -> com.sasharx.unitymeshlab (1.0.0) BREAKING CHANGE: package identifier and repository URL changed. Downstream consumers must update the entry in Packages/manifest.json: "com.sasharx.unitymeshlab": "https://github.com/SashaRX/UnityMeshLab.git" Also adds documentationUrl / changelogUrl / licensesUrl fields and bumps version 0.15.58 -> 1.0.0 to signal package-id stabilization. CHANGELOG entry documents the migration path for namespace and manifest consumers. --- CHANGELOG.md | 22 ++++++++++++++++++++++ package.json | 9 ++++++--- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a63d225..be6adb86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +All notable changes to this project are documented in this file. +The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/). + +## [1.0.0] - 2026-04-20 + +### Changed (breaking) +- **Package identifier** renamed from `com.sasharx.lightmap-uv-tool` to `com.sasharx.unitymeshlab` to align with the repository name and the canonical `com.sasharx.` rule in `.claude/skills/_shared/naming-conventions.md`. Downstream consumers must update the entry in `Packages/manifest.json`. +- **Root namespace** migrated from `LightmapUvTool` to `SashaRX.UnityMeshLab` across all 48 Editor C# files. The asmdef `name` and `rootNamespace` were updated and the asmdef file was renamed `LightmapUvTool.Editor.asmdef` → `SashaRX.UnityMeshLab.Editor.asmdef` (GUID preserved). External code that referenced the bare `LightmapUvTool` namespace must switch to `SashaRX.UnityMeshLab`. +- **`repository.url`** updated from `UnityLodUvLightmapTransfer.git` to `UnityMeshLab.git` to match the current canonical GitHub URL. + +### Added +- `documentationUrl`, `changelogUrl`, and `licensesUrl` fields in `package.json`. +- `.claude/skills/` overhaul — English-language skill catalog with progressive disclosure, `_shared/` references, `_checklists/`, `_template/package-template/`, and three new skills (`unity-ci-validation`, `unity-package-bootstrap`, `repo-conventions`). +- `.prettierignore` protecting skill YAML frontmatter from Prettier reflow. + +### Migration guide +- In consumer projects, replace the old identifier in `Packages/manifest.json`: + ```json + "com.sasharx.unitymeshlab": "https://github.com/SashaRX/UnityMeshLab.git" + ``` +- In any code that referenced the `LightmapUvTool` namespace, switch to `SashaRX.UnityMeshLab`. + ## [0.15.36] - 2026-04-07 ### Added — Cleanup tool (#59) diff --git a/package.json b/package.json index 6bced843..7a2b77d7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "com.sasharx.lightmap-uv-tool", - "version": "0.15.58", + "name": "com.sasharx.unitymeshlab", + "version": "1.0.0", "displayName": "Mesh Lab", "description": "Unity Editor tool suite: UV2 lightmap transfer, LOD generation, UV analysis, and FBX export with binary format support.", "unity": "6000.0", @@ -10,8 +10,11 @@ }, "repository": { "type": "git", - "url": "https://github.com/SashaRX/UnityLodUvLightmapTransfer.git" + "url": "https://github.com/SashaRX/UnityMeshLab.git" }, + "documentationUrl": "https://github.com/SashaRX/UnityMeshLab", + "changelogUrl": "https://github.com/SashaRX/UnityMeshLab/blob/main/CHANGELOG.md", + "licensesUrl": "https://github.com/SashaRX/UnityMeshLab/blob/main/LICENSE", "keywords": [ "lightmap", "uv", From 2dc054d0c8be2d4ac94d4d723bca4f7588586d9a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Apr 2026 22:49:46 +0000 Subject: [PATCH 17/21] docs(skills): repo-conventions reflects resolved 1.0.0 state --- .claude/skills/repo-conventions/SKILL.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.claude/skills/repo-conventions/SKILL.md b/.claude/skills/repo-conventions/SKILL.md index 6d6d29af..824bf447 100644 --- a/.claude/skills/repo-conventions/SKILL.md +++ b/.claude/skills/repo-conventions/SKILL.md @@ -41,10 +41,7 @@ Missing and planned (see `unity-ci-validation/SKILL.md`): ## Deviations from `_shared/naming-conventions.md` -Both items below are known deviations captured for tracking; the migration to the canonical values is planned (see "Migration status" below). - -- **`package.json` `name`** is currently `com.sasharx.lightmap-uv-tool` — does NOT match the repository folder name `UnityMeshLab`. Canonical value: `com.sasharx.unitymeshlab`. Rationale for deviation: historical — the repo originated as `lightmap-uv-tool` before the mesh-lab rename. Migration breaks downstream consumers; schedule under a MAJOR SemVer bump. -- *(Resolved in the `claude/skills-overhaul-phase-0-Xrg7K` branch: namespace migrated from bare `LightmapUvTool` to `SashaRX.UnityMeshLab` across 48 `.cs` files; asmdef renamed `LightmapUvTool.Editor.asmdef` → `SashaRX.UnityMeshLab.Editor.asmdef`, GUID preserved.)* +None at the canonical-target level. Both historical deviations (bare `LightmapUvTool` namespace, `com.sasharx.lightmap-uv-tool` package id) were resolved in the 1.0.0 release. ## Primary domain vocabulary @@ -66,9 +63,9 @@ Shared with agents via `CLAUDE.md`: - Call `RestoreWorkingMeshes()` before clearing/switching LODGroup context. - Destroy temporary meshes (repacked, transferred, welded) when no longer needed. -## Migration status +## Migration history -Namespace migration to `SashaRX.UnityMeshLab` is complete. The `package.json` `name` rename from `com.sasharx.lightmap-uv-tool` to `com.sasharx.unitymeshlab` remains scheduled — it is a downstream-breaking change and must ship under a MAJOR SemVer bump with explicit user communication. +- **1.0.0 (2026-04-20)** — package id renamed `com.sasharx.lightmap-uv-tool` → `com.sasharx.unitymeshlab`; namespace renamed `LightmapUvTool` → `SashaRX.UnityMeshLab`; repository URL corrected to `UnityMeshLab.git`. Downstream migration steps in `CHANGELOG.md`. ## Further reading From 2310d74ff8ae10de3e0d8d3f9f55fab7f876b2d5 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Apr 2026 22:50:46 +0000 Subject: [PATCH 18/21] ci: add GameCI test workflow and tag-triggered release workflow test.yml - EditMode test run on Unity 6000.0.33f1 (matches package.json 'unity: 6000.0'). - Single matrix row today; extend when a second LTS is adopted. fail-fast: false. - Skips for fork PRs to avoid UNITY_LICENSE-missing failures. - Library cache keyed on package.json + asmdef + cs hash; concurrency cancels superseded runs on the same ref. - Uploads NUnit XML artifacts even on failure. release.yml - Triggered on push of v* tag. - Verifies tag matches package.json version (exact string). - Extracts the matching '## [version]' section from CHANGELOG.md as release body; falls back to GitHub auto-generated notes if absent. - Marks as prerelease when the tag contains a dash (e.g. v1.0.0-rc1). Required secrets (repository-level): - UNITY_LICENSE (personal .ulf, base64-encoded, single line) OR - UNITY_SERIAL + UNITY_EMAIL + UNITY_PASSWORD (professional). Both workflows are authored per unity-ci-validation/SKILL.md canonical skeleton. --- .claude/skills/repo-conventions/SKILL.md | 7 +-- .github/workflows/release.yml | 61 +++++++++++++++++++ .github/workflows/test.yml | 76 ++++++++++++++++++++++++ 3 files changed, 139 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/test.yml diff --git a/.claude/skills/repo-conventions/SKILL.md b/.claude/skills/repo-conventions/SKILL.md index 824bf447..dd7cf4ef 100644 --- a/.claude/skills/repo-conventions/SKILL.md +++ b/.claude/skills/repo-conventions/SKILL.md @@ -33,11 +33,8 @@ Located under `.github/workflows/`: - `build-native.yml` — builds native plugin binaries (platform-specific). - `meta-check.yml` — verifies `.meta` file coverage. - `version-bump.yml` — automates `package.json` version bumps. - -Missing and planned (see `unity-ci-validation/SKILL.md`): - -- `test.yml` — EditMode/PlayMode matrix on 6000.0 minimum. -- `release.yml` — tag-triggered GitHub Release. +- `test.yml` — EditMode test run on Unity 6000.0. Requires `UNITY_LICENSE` (personal) OR `UNITY_SERIAL` + `UNITY_EMAIL` + `UNITY_PASSWORD` (professional) repository secrets. See `unity-ci-validation/SKILL.md`. +- `release.yml` — tag-triggered GitHub Release; verifies `v` tag matches `package.json` and extracts the matching section from `CHANGELOG.md`. ## Deviations from `_shared/naming-conventions.md` diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..c7efaa31 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,61 @@ +name: Release + +on: + push: + tags: + - 'v*' + workflow_dispatch: + +permissions: + contents: write + +jobs: + github-release: + name: Create GitHub Release + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Verify tag matches package.json version + run: | + TAG="${GITHUB_REF_NAME#v}" + PKG_VERSION=$(jq -r '.version' package.json) + if [ "$TAG" != "$PKG_VERSION" ]; then + echo "::error::Tag $GITHUB_REF_NAME does not match package.json version $PKG_VERSION" + exit 1 + fi + echo "Tag $GITHUB_REF_NAME matches package.json version $PKG_VERSION" + + - name: Extract changelog section for this version + id: changelog + run: | + VERSION="${GITHUB_REF_NAME#v}" + awk -v v="## [$VERSION]" ' + $0 ~ v {capturing=1; next} + /^## \[/ && capturing {exit} + capturing {print} + ' CHANGELOG.md > /tmp/release-notes.md + if [ ! -s /tmp/release-notes.md ]; then + echo "::warning::No CHANGELOG section found for $VERSION; using auto-generated notes." + echo "has_notes=false" >> "$GITHUB_OUTPUT" + else + echo "has_notes=true" >> "$GITHUB_OUTPUT" + { + echo "body<> "$GITHUB_OUTPUT" + fi + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.ref_name }} + name: ${{ github.ref_name }} + body: ${{ steps.changelog.outputs.has_notes == 'true' && steps.changelog.outputs.body || '' }} + generate_release_notes: ${{ steps.changelog.outputs.has_notes != 'true' }} + draft: false + prerelease: ${{ contains(github.ref_name, '-') }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..2d3ca39f --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,76 @@ +name: Unity Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + editmode: + name: EditMode / Unity ${{ matrix.unity-version }} + runs-on: ubuntu-latest + # Test workflow requires UNITY_LICENSE (personal) OR UNITY_SERIAL + UNITY_EMAIL + + # UNITY_PASSWORD (professional). Skip gracefully if absent so PRs from forks + # don't fail on the missing secret. + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} + strategy: + fail-fast: false + matrix: + unity-version: ['6000.0.33f1'] + test-mode: ['editmode'] + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + lfs: true + + - name: Cache Library + uses: actions/cache@v4 + with: + path: Library + key: Library-${{ matrix.unity-version }}-${{ hashFiles('package.json', '**/*.asmdef', '**/*.cs') }} + restore-keys: | + Library-${{ matrix.unity-version }}- + Library- + + - name: Run EditMode tests + uses: game-ci/unity-test-runner@v4 + env: + UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} + UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} + UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} + UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} + with: + unityVersion: ${{ matrix.unity-version }} + testMode: ${{ matrix.test-mode }} + artifactsPath: artifacts/${{ matrix.unity-version }}-${{ matrix.test-mode }} + githubToken: ${{ secrets.GITHUB_TOKEN }} + checkName: EditMode ${{ matrix.unity-version }} + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results-${{ matrix.unity-version }}-${{ matrix.test-mode }} + path: artifacts/**/*.xml + if-no-files-found: warn + + summary: + name: Test summary + needs: editmode + if: always() + runs-on: ubuntu-latest + steps: + - name: Report + run: | + echo "EditMode outcome: ${{ needs.editmode.result }}" + if [ "${{ needs.editmode.result }}" = "failure" ]; then + exit 1 + fi From b9165d566b1a75d0b4035349b85d907fd82a85d6 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Apr 2026 22:52:38 +0000 Subject: [PATCH 19/21] ci(test): add license-presence gate so workflow skips cleanly when secrets absent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously the 'EditMode' matrix job failed within 5 seconds because GameCI fails fast when UNITY_LICENSE (and UNITY_SERIAL/…) are all empty. That produced a red CI check on every PR until secrets are configured. Restructured: - New 'check-license' job probes 'secrets.UNITY_LICENSE != ''' and 'secrets.UNITY_SERIAL != '' && UNITY_EMAIL != '' && UNITY_PASSWORD != '''. Outputs 'has-license' true/false. Emits an Actions notice + step-summary explaining how to add the secret when missing. - 'editmode' job gates on 'needs.check-license.outputs.has-license == 'true'' AND the existing fork-PR guard. Skips cleanly otherwise. - 'summary' depends on both; treats 'skipped' as OK, fails only on real 'failure'. That keeps the overall workflow green when no license is configured, and red when tests legitimately fail. --- .github/workflows/test.yml | 53 +++++++++++++++++++++++++++++++++----- 1 file changed, 46 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2d3ca39f..1f2dcfa7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,13 +12,42 @@ concurrency: cancel-in-progress: true jobs: + check-license: + name: Check Unity license + runs-on: ubuntu-latest + outputs: + has-license: ${{ steps.check.outputs.has-license }} + steps: + - name: Probe secrets + id: check + env: + HAS_PERSONAL: ${{ secrets.UNITY_LICENSE != '' }} + HAS_PRO: ${{ secrets.UNITY_SERIAL != '' && secrets.UNITY_EMAIL != '' && secrets.UNITY_PASSWORD != '' }} + run: | + if [ "$HAS_PERSONAL" = "true" ] || [ "$HAS_PRO" = "true" ]; then + echo "has-license=true" >> "$GITHUB_OUTPUT" + echo "Unity license credentials present — will run tests." + else + echo "has-license=false" >> "$GITHUB_OUTPUT" + { + echo "### Unity tests skipped" + echo "" + echo "No Unity license secrets configured in this repository." + echo "" + echo "To enable this workflow, add one of:" + echo "- \`UNITY_LICENSE\` (base64-encoded personal \`.ulf\`, single line), OR" + echo "- \`UNITY_SERIAL\` + \`UNITY_EMAIL\` + \`UNITY_PASSWORD\` (professional)." + echo "" + echo "See \`.claude/skills/unity-ci-validation/SKILL.md\` for the activation recipe." + } >> "$GITHUB_STEP_SUMMARY" + echo "::notice::UNITY_LICENSE (or UNITY_SERIAL/UNITY_EMAIL/UNITY_PASSWORD) not configured — skipping Unity test job." + fi + editmode: name: EditMode / Unity ${{ matrix.unity-version }} + needs: check-license + if: needs.check-license.outputs.has-license == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) runs-on: ubuntu-latest - # Test workflow requires UNITY_LICENSE (personal) OR UNITY_SERIAL + UNITY_EMAIL + - # UNITY_PASSWORD (professional). Skip gracefully if absent so PRs from forks - # don't fail on the missing secret. - if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} strategy: fail-fast: false matrix: @@ -64,13 +93,23 @@ jobs: summary: name: Test summary - needs: editmode + needs: [check-license, editmode] if: always() runs-on: ubuntu-latest steps: - name: Report + env: + LICENSE_RESULT: ${{ needs.check-license.result }} + EDITMODE_RESULT: ${{ needs.editmode.result }} run: | - echo "EditMode outcome: ${{ needs.editmode.result }}" - if [ "${{ needs.editmode.result }}" = "failure" ]; then + echo "check-license: $LICENSE_RESULT" + echo "editmode: $EDITMODE_RESULT" + # Fail only on an actual test failure. 'skipped' (no license secret) is OK. + if [ "$EDITMODE_RESULT" = "failure" ]; then + echo "::error::EditMode tests failed" + exit 1 + fi + if [ "$LICENSE_RESULT" = "failure" ]; then + echo "::error::check-license job failed" exit 1 fi From f45ad6ad5a2dc8404ca4fb8afe28a61d9198db57 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Apr 2026 14:30:59 +0000 Subject: [PATCH 20/21] docs(skills): document Unity Personal CI reality (manual activation disabled in 2024) unity-ci-validation/SKILL.md - Promote a 'License activation' section that opens with the Personal-tier blocker: Unity disabled manual .alf/.ulf activation for Personal seats in 2024. game-ci/unity-request-activation-file is dead for Personal. - Document three realistic Personal-tier paths: license-gate (default), reuse a locally-extracted .ulf (low success), self-hosted runner. - Document Pro/Plus path with UNITY_SERIAL + UNITY_EMAIL + UNITY_PASSWORD. - Add the canonical license-gate workflow pattern as a copy-paste recipe, with the env-var indirection trick that lets 'secrets.X != ''' resolve before masking. Same pattern used in this repo's .github/workflows/test.yml. - Expand 'Debugging failed runs': separate Personal vs Pro license-failure diagnoses; add the '~5-second EditMode failure' signature for missing secrets without a gate. repo-conventions/SKILL.md - Note that this repo is on Personal tier; the test.yml job will stay 'skipped' on GitHub-hosted runners until Sasha moves to Plus/Pro or sets up a self-hosted runner. Local Test Runner remains the verification path. --- .claude/skills/repo-conventions/SKILL.md | 2 +- .claude/skills/unity-ci-validation/SKILL.md | 71 +++++++++++++++++++-- 2 files changed, 66 insertions(+), 7 deletions(-) diff --git a/.claude/skills/repo-conventions/SKILL.md b/.claude/skills/repo-conventions/SKILL.md index dd7cf4ef..7bda3eb2 100644 --- a/.claude/skills/repo-conventions/SKILL.md +++ b/.claude/skills/repo-conventions/SKILL.md @@ -33,7 +33,7 @@ Located under `.github/workflows/`: - `build-native.yml` — builds native plugin binaries (platform-specific). - `meta-check.yml` — verifies `.meta` file coverage. - `version-bump.yml` — automates `package.json` version bumps. -- `test.yml` — EditMode test run on Unity 6000.0. Requires `UNITY_LICENSE` (personal) OR `UNITY_SERIAL` + `UNITY_EMAIL` + `UNITY_PASSWORD` (professional) repository secrets. See `unity-ci-validation/SKILL.md`. +- `test.yml` — EditMode test run on Unity 6000.0. License-gated: skips cleanly when no `UNITY_LICENSE`/`UNITY_SERIAL` secrets are configured. **This repository is on Unity Personal (free) tier**, and Unity disabled manual `.alf`→`.ulf` activation for Personal seats in 2024, so the test job is currently always **skipped** on GitHub-hosted runners. Local Test Runner remains the canonical pre-commit verification path. See `unity-ci-validation/SKILL.md` §License activation for the recipe and the path forward (self-hosted runner or Plus/Pro upgrade). - `release.yml` — tag-triggered GitHub Release; verifies `v` tag matches `package.json` and extracts the matching section from `CHANGELOG.md`. ## Deviations from `_shared/naming-conventions.md` diff --git a/.claude/skills/unity-ci-validation/SKILL.md b/.claude/skills/unity-ci-validation/SKILL.md index e24f2810..143ad4bf 100644 --- a/.claude/skills/unity-ci-validation/SKILL.md +++ b/.claude/skills/unity-ci-validation/SKILL.md @@ -93,16 +93,73 @@ Rules: ## License activation -Two modes: +> **Important.** As of 2024, Unity disabled manual activation of Personal licenses. The legacy `game-ci/unity-request-activation-file` → upload `.alf` to `license.unity3d.com` → download `.ulf` flow only works for **Pro / Plus / Enterprise** seats. Personal-tier projects must either reuse a locally-activated `.ulf` (machine-bound — usually fails on a GitHub-hosted runner), use a self-hosted runner with a pre-activated Unity install, or accept that Unity test execution is unavailable in CI on this tier. -- **Personal license** — obtain an activation file via `game-ci/unity-request-activation-file`; encode to base64 and store in `UNITY_LICENSE` secret. Shown in the `UNITY_LICENSE` env above. -- **Professional license** — set `UNITY_SERIAL`, `UNITY_EMAIL`, and `UNITY_PASSWORD` secrets; `unity-test-runner` picks them up. +### Personal (free) tier — three realistic paths -Secrets layout (minimum scopes): +1. **License-gate the workflow** (default for free-tier repos). Skip the Unity job cleanly when no credentials are present so CI stays green; metadata, `.meta`, and namespace checks still run on every push and PR. Recipe in the next section. +2. **Reuse a locally-activated `.ulf`.** Possible to try, low success rate. Personal `.ulf` files are bound to the original machine's hardware ID; GitHub-hosted runners typically fail validation. Worth attempting once before giving up. + - Windows: `C:\ProgramData\Unity\Unity_lic.ulf` + - macOS: `/Library/Application Support/Unity/Unity_lic.ulf` + - Linux: `~/.local/share/unity3d/Unity/Unity_lic.ulf` + - `base64 -w0 Unity_lic.ulf` → paste as `UNITY_LICENSE` repository secret. +3. **Self-hosted runner.** A workstation that already has Unity Personal activated runs the GitHub Actions runner agent. Bypasses the licensing gate entirely. Overkill for small packages, sensible for active solo projects. + +### Professional / Plus tier + +- Set `UNITY_SERIAL`, `UNITY_EMAIL`, and `UNITY_PASSWORD` repository secrets; `unity-test-runner` picks them up automatically. +- License is checked out at job start and returned at job end; concurrent jobs share the same seat sequentially (GameCI handles serialization). +- Treat the serial as long-lived but rotate immediately if leaked. + +### License-gate pattern (canonical for any tier) + +Probe the secret presence in a separate job; gate the test matrix on the result. This is the only pattern that keeps CI green across all of: PR from a fork (no secret access), fresh clone before secret setup, and Personal-tier repos that cannot activate at all. + +```yaml +jobs: + check-license: + runs-on: ubuntu-latest + outputs: + has-license: ${{ steps.check.outputs.has-license }} + steps: + - id: check + env: + HAS_PERSONAL: ${{ secrets.UNITY_LICENSE != '' }} + HAS_PRO: ${{ secrets.UNITY_SERIAL != '' && secrets.UNITY_EMAIL != '' && secrets.UNITY_PASSWORD != '' }} + run: | + if [ "$HAS_PERSONAL" = "true" ] || [ "$HAS_PRO" = "true" ]; then + echo "has-license=true" >> "$GITHUB_OUTPUT" + else + echo "has-license=false" >> "$GITHUB_OUTPUT" + echo "::notice::Unity license secrets not configured — skipping test job." + fi + + editmode: + needs: check-license + if: needs.check-license.outputs.has-license == 'true' + # ... game-ci/unity-test-runner step ... + + summary: + needs: [check-license, editmode] + if: always() + runs-on: ubuntu-latest + steps: + - run: | + # 'skipped' is OK (no license); only real 'failure' fails the check. + [ "${{ needs.editmode.result }}" = "failure" ] && exit 1 || true +``` + +Notes: + +- `secrets.X != ''` evaluates BEFORE secret masking, so the comparison works even when the secret is empty/unset. The masked value never appears in logs. +- Pass the comparison through an `env:` block, not a direct `${{ secrets.X }}` interpolation in `run:` — keeps the runner shell from ever touching the raw secret. +- The `summary` job exists so the overall workflow check is green when the matrix legitimately skips. Without it, a `needs` graph with all-skipped leaves shows as "skipped" on the PR check, which some branch-protection rules interpret as failure. + +### Secrets layout (minimum scopes) - `UNITY_LICENSE` (personal) OR `UNITY_SERIAL` + `UNITY_EMAIL` + `UNITY_PASSWORD` (professional). - `GITHUB_TOKEN` is automatic for standard actions. -- Never log secret contents. The GameCI action redacts by default; custom `run:` steps must not `echo` them. +- Never log secret contents. The GameCI action redacts by default; custom `run:` steps must not `echo` them. Use the env-var indirection pattern shown above. ## Batch-mode invocations @@ -209,7 +266,9 @@ Use `cycjimmy/semantic-release-action@v4` with a Unity-specific plugin list in ` ## Debugging failed runs -- **License activation failure** → check the secret's base64 encoding is single-line, no trailing newline. +- **License activation failure on Personal tier** → expected as of 2024; Unity disabled manual `.alf`→`.ulf` activation for Personal seats. Use the license-gate pattern in §License activation, or move to a self-hosted runner. A locally-extracted `.ulf` from `Unity_lic.ulf` may work but is hardware-bound and usually fails on GitHub-hosted runners. +- **License activation failure on Pro tier** → check the secret's base64 encoding is single-line, no trailing newline; verify `UNITY_EMAIL` and `UNITY_PASSWORD` belong to the same Unity ID seat that owns the serial. +- **EditMode job fails in ~5 seconds** → GameCI's fast-fail when license env vars are all empty. Add the license-gate job (§License activation) so the matrix skips instead of failing. - **Library cache miss on every run** → the cache key includes a hash of a file that changes every run; narrow the hash inputs. - **Tests pass locally but fail in CI** → check `Application.isBatchMode` guards; check that tests do not depend on scene objects that exist only when the editor opens interactively. - **`EditorApplication.Exit(0)` reached too early** → a test assembly failed to compile and batch mode short-circuits; check the log for `error CS`. From 8fa8fb8500e2d69ea38bfe3f91b8e773ccd9b30e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Apr 2026 14:36:28 +0000 Subject: [PATCH 21/21] ci(release): use awk index() for changelog header match (was always-empty regex) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex reported that 'awk -v v="## [$VERSION]" $0 ~ v' treats v as a regex, so a SemVer header like '## [1.0.0]' parses as the literal '## ' followed by the character class [1.0.0] — i.e. any one of {1, ., 0}. On the real heading '## [1.0.0] - 2026-04-20' the next char after '## ' is '[', which is not in the class, so the match never fires. Result: /tmp/release-notes.md stays empty and every release falls back to GitHub auto-generated notes instead of the curated CHANGELOG section. Replace the regex match with awk's index() function anchored at column 1: literal substring search, no metacharacter handling needed. Verified locally on the real CHANGELOG.md — extracts the 18-line '## [1.0.0]' section and stops at the next '## [' heading as intended. Reported by chatgpt-codex-connector on PR #106 review (P2). --- .github/workflows/release.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c7efaa31..8660437b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,8 +33,11 @@ jobs: id: changelog run: | VERSION="${GITHUB_REF_NAME#v}" - awk -v v="## [$VERSION]" ' - $0 ~ v {capturing=1; next} + HEADER="## [$VERSION]" + # Use awk index() (literal substring) anchored at column 1 — regex match + # would treat '[' '.' ']' in a SemVer header as metacharacters and never fire. + awk -v header="$HEADER" ' + index($0, header) == 1 {capturing=1; next} /^## \[/ && capturing {exit} capturing {print} ' CHANGELOG.md > /tmp/release-notes.md