diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index ad77e20..0022f25 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -328,8 +328,6 @@ jobs: "com.forkhorizon.nexus.unity", "com.unity.inputsystem", "com.unity.nuget.newtonsoft-json", - "com.unity.project-auditor", - "com.unity.project-auditor-rules", "com.unity.test-framework", ] missing = [package for package in required if package not in dependencies] @@ -337,7 +335,16 @@ jobs: print(f"::error::Unity package smoke did not resolve dependencies: {', '.join(missing)}") sys.exit(1) - print("Unity package smoke resolved Nexus Unity and required dependencies.") + forbidden = [ + "com.unity.project-auditor", + "com.unity.project-auditor-rules", + ] + present_forbidden = [package for package in forbidden if package in dependencies] + if present_forbidden: + print(f"::error::Unity package smoke resolved forbidden Project Auditor dependencies: {', '.join(present_forbidden)}") + sys.exit(1) + + print("Unity package smoke resolved Nexus Unity and required dependencies without Project Auditor packages.") PY - name: Run package Editor tests diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dfcf23..0ac04f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable public changes to Nexus Unity are documented here. ## [Unreleased] +### Fixed +- Removed direct Unity Project Auditor package dependencies and made Unity Project Auditor execution optional, so clean installs avoid duplicate immutable `.meta` GUID warnings and `Could not find any registered modules` Console spam. + ### Changed - Added a repository mailmap entry for `air17` so local Git contributor reports resolve historical `air17@github.com` commits to the GitHub account's canonical no-reply identity. diff --git a/DOCUMENTATION.MD b/DOCUMENTATION.MD index 109b119..32f7b4a 100644 --- a/DOCUMENTATION.MD +++ b/DOCUMENTATION.MD @@ -173,7 +173,7 @@ The same job also runs `NexusQualityGate --checklist-ai required`. That mode rea The reviewer sends `NEXUS_DOC_AI_KEEP_ALIVE` to Ollama on each request, defaults it to `30s`, and unloads the model at the end of the review. The workflow also has an `always()` cleanup step that unloads the same model even when validation fails. For pull requests, the job executes the quality tool from the trusted base branch and treats the pull request checkout as input data. While bootstrapping quality-gate changes, it falls back to the candidate tool when the trusted base branch does not contain `NexusQualityGate` yet or has not learned the `--checklist-ai` option. -The Unity smoke job creates a temporary project under `$RUNNER_TEMP`, installs the candidate package through `file:$GITHUB_WORKSPACE`, imports it with the local Unity editor, scans the Unity log for C# compiler errors and orphan/immutable `.meta` warnings, and verifies that package dependencies resolved in `packages-lock.json`. +The Unity smoke job creates a temporary project under `$RUNNER_TEMP`, installs the candidate package through `file:$GITHUB_WORKSPACE`, imports it with the local Unity editor, scans the Unity log for C# compiler errors and orphan/immutable `.meta` warnings, and verifies that required package dependencies resolved in `packages-lock.json` without Unity Project Auditor packages. Full local integration validation is explicit: @@ -208,6 +208,7 @@ Before release, maintainers should run a public API stress audit that compares r - Repository funding metadata lives in `.github/FUNDING.yml` and configures the GitHub Sponsor button for `Daliys`. - Do not ship generated caches, local agent folders, `.jules/`, `.DS_Store`, Python bytecode, or native bridge binaries without corresponding source and build instructions. - Keep contributor tooling and package-internal tests under Unity-ignored folders such as `tools~/` and `Tests~/` without `.meta` files; Unity will not import those folders, and root `~.meta` entries break immutable PackageCache installs. +- Do not declare Unity Project Auditor packages as Nexus dependencies. Nexus audit support uses reflection when a host project explicitly installs compatible Project Auditor rules; direct dependencies can add duplicate immutable `.meta` GUID warnings or no-module Console spam during clean installs. ## Development Versioning Policy diff --git a/Editor/ProjectAuditorFinal.cs b/Editor/ProjectAuditorFinal.cs index f9b30c5..486c6c9 100644 --- a/Editor/ProjectAuditorFinal.cs +++ b/Editor/ProjectAuditorFinal.cs @@ -17,6 +17,8 @@ namespace UnityMCP.Editor /// public static class ProjectAuditorWrapper { + private const string ProjectAuditorRulesPackageName = "com.unity.project-auditor-rules"; + private static readonly List _componentCache = new List(); private static readonly List _rendererCache = new List(); private static readonly List _materialCache = new List(); @@ -45,136 +47,8 @@ public static string RunAudit(bool silent) try { - var assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.GetName().Name.Contains("ProjectAuditor")); - if (assembly != null) - { - var auditorType = assembly.GetType("Unity.ProjectAuditor.Editor.ProjectAuditor"); - var paramsType = assembly.GetType("Unity.ProjectAuditor.Editor.AnalysisParams"); - - if (auditorType != null && paramsType != null) - { - var auditor = Activator.CreateInstance(auditorType); - var analysisParams = Activator.CreateInstance(paramsType, new object[] { true }); - - var auditMethod = auditorType.GetMethods().FirstOrDefault(m => m.Name == "Audit" && m.GetParameters().Length == 2); - if (auditMethod != null) - { - var report = auditMethod.Invoke(auditor, new object[] { analysisParams, null }); - if (report != null) - { - // Determine if we are in the Nexus sandbox or a user project - bool isSandbox = System.IO.Directory.Exists("Assets/NexusUnity"); - string targetPath = isSandbox ? "Assets/NexusUnity" : "Assets"; - NexusEditorLog.Log(NexusLogCategory.Audit, $"[Nexus Audit] START - isSandbox: {isSandbox}, targetPath: {targetPath}"); - - var getAllIssuesMethod = report.GetType().GetMethod("GetAllIssues"); - var allIssues = (System.Collections.IEnumerable)getAllIssuesMethod.Invoke(report, null); - - var codeIssues = new JArray(); - if (allIssues != null) - { - // Sandbox-specific noise reduction filters - string[] sandboxIgnorePatterns = { - "Newtonsoft.Json", "allocation", "usage", "System.Reflection", - "System.Linq", "System.String.Concat", "ref type", "Closure", - "UnityEngine.Object.name", "Debug.Log", "Implicit", "GetEntityId" - }; - - foreach (var issue in allIssues) - { - var t = issue.GetType(); - string category = t.GetProperty("Category")?.GetValue(issue)?.ToString() ?? "Unknown"; - string description = t.GetProperty("Description")?.GetValue(issue)?.ToString() ?? "No description"; - - var location = t.GetProperty("Location")?.GetValue(issue); - string filePath = ""; - - if (location != null) - { - var locType = location.GetType(); - filePath = locType.GetProperty("Path")?.GetValue(location)?.ToString() ?? ""; - } - - // 1. Path Filtering - if (category.Contains("Code")) - { - if (string.IsNullOrEmpty(filePath) || !filePath.StartsWith(targetPath)) - { - continue; - } - - // 2. Sandbox Noise Reduction (Only active when developing Nexus) - if (isSandbox) - { - bool shouldIgnore = false; - foreach (var pattern in sandboxIgnorePatterns) - { - if (description.IndexOf(pattern, StringComparison.OrdinalIgnoreCase) >= 0) { shouldIgnore = true; break; } - } - if (shouldIgnore) continue; - } - } - else - { - // Ignore general project noise (outdated packages, etc.) in the sandbox - if (isSandbox) - { - if (string.IsNullOrEmpty(filePath) || (!filePath.Contains("com.forkhorizon.nexus.unity") && !filePath.Contains("Assets/NexusUnity"))) - { - continue; - } - } - } - - var i = new JObject(); - i["category"] = category; - i["description"] = description; - i["file"] = filePath; - - if (location != null) - { - var locType = location.GetType(); - i["line"] = locType.GetProperty("Line")?.GetValue(location)?.ToString(); - } - - codeIssues.Add(i); - } - } - result["code_issues"] = codeIssues; - result["num_total_issues"] = codeIssues.Count; - NexusEditorLog.Log(NexusLogCategory.Audit, $"[Nexus Audit] END - Total Filtered: {codeIssues.Count}", true); - } - } - } - } - - // --- Custom Nexus Style Audit --- - string customTargetPath = System.IO.Directory.Exists("Assets/NexusUnity") ? "Assets/NexusUnity" : "Assets"; - var codeIssuesList = result["code_issues"] as JArray ?? new JArray(); - NexusEditorLog.Log(NexusLogCategory.Audit, $"[Nexus Style Audit] Scanning path: {customTargetPath}, current issues: {codeIssuesList.Count}"); - - string[] files = System.IO.Directory.GetFiles(customTargetPath, "*.cs", System.IO.SearchOption.AllDirectories); - int styleIssuesAdded = 0; - foreach (var file in files) - { - string relativePath = file.Replace(System.IO.Directory.GetCurrentDirectory() + "/", "").Replace("\\", "/"); - if (relativePath.Contains("Assets/")) relativePath = relativePath.Substring(relativePath.IndexOf("Assets/")); - - string[] lines = System.IO.File.ReadAllLines(file); - if (lines.Length > 300) - { - codeIssuesList.Add(new JObject { - ["category"] = "Style", - ["description"] = $"File exceeds 300 lines limit (Current: {lines.Length} lines).", - ["file"] = relativePath, - ["line"] = "1" - }); - styleIssuesAdded++; - } - } - result["code_issues"] = codeIssuesList; - result["num_total_issues"] = codeIssuesList.Count; - NexusEditorLog.Log(NexusLogCategory.Audit, $"[Nexus Style Audit] Added {styleIssuesAdded} style issues. Total: {codeIssuesList.Count}", true); + RunUnityProjectAuditor(result, silent); + RunNexusStyleAudit(result); } catch (Exception e) { @@ -189,6 +63,159 @@ public static string RunAudit(bool silent) return result.ToString(); } + private static void RunUnityProjectAuditor(JObject result, bool silent) + { + var assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.GetName().Name.Contains("ProjectAuditor")); + if (assembly == null || !ShouldRunUnityProjectAuditor(silent)) + { + return; + } + + var auditorType = assembly.GetType("Unity.ProjectAuditor.Editor.ProjectAuditor"); + var paramsType = assembly.GetType("Unity.ProjectAuditor.Editor.AnalysisParams"); + if (auditorType == null || paramsType == null) + { + return; + } + + var auditMethod = auditorType.GetMethods().FirstOrDefault(m => m.Name == "Audit" && m.GetParameters().Length == 2); + if (auditMethod == null) + { + return; + } + + var auditor = Activator.CreateInstance(auditorType); + var analysisParams = Activator.CreateInstance(paramsType, new object[] { true }); + var report = auditMethod.Invoke(auditor, new object[] { analysisParams, null }); + if (report == null) + { + return; + } + + bool isSandbox = Directory.Exists("Assets/NexusUnity"); + string targetPath = isSandbox ? "Assets/NexusUnity" : "Assets"; + NexusEditorLog.Log(NexusLogCategory.Audit, $"[Nexus Audit] START - isSandbox: {isSandbox}, targetPath: {targetPath}"); + + var getAllIssuesMethod = report.GetType().GetMethod("GetAllIssues"); + var allIssues = (System.Collections.IEnumerable)getAllIssuesMethod.Invoke(report, null); + var codeIssues = CollectProjectAuditorIssues(allIssues, isSandbox, targetPath); + result["code_issues"] = codeIssues; + result["num_total_issues"] = codeIssues.Count; + NexusEditorLog.Log(NexusLogCategory.Audit, $"[Nexus Audit] END - Total Filtered: {codeIssues.Count}", true); + } + + private static bool ShouldRunUnityProjectAuditor(bool silent) + { + bool hasRulesPackage = IsPackageResolved(ProjectAuditorRulesPackageName); + if (!hasRulesPackage && !silent) + { + NexusEditorLog.Log(NexusLogCategory.Audit, "[Nexus Audit] Skipping Unity Project Auditor because no Project Auditor rules package is resolved."); + } + + return hasRulesPackage; + } + + private static bool IsPackageResolved(string packageName) + { + try + { + return UnityEditor.PackageManager.PackageInfo.FindForPackageName(packageName) != null; + } + catch + { + return false; + } + } + + private static JArray CollectProjectAuditorIssues(System.Collections.IEnumerable allIssues, bool isSandbox, string targetPath) + { + var codeIssues = new JArray(); + if (allIssues == null) + { + return codeIssues; + } + + string[] sandboxIgnorePatterns = { "Newtonsoft.Json", "allocation", "usage", "System.Reflection", "System.Linq", "System.String.Concat", "ref type", "Closure", "UnityEngine.Object.name", "Debug.Log", "Implicit", "GetEntityId" }; + + foreach (var issue in allIssues) + { + AddProjectAuditorIssue(codeIssues, issue, isSandbox, targetPath, sandboxIgnorePatterns); + } + + return codeIssues; + } + + private static void AddProjectAuditorIssue(JArray codeIssues, object issue, bool isSandbox, string targetPath, string[] sandboxIgnorePatterns) + { + var t = issue.GetType(); + string category = t.GetProperty("Category")?.GetValue(issue)?.ToString() ?? "Unknown"; + string description = t.GetProperty("Description")?.GetValue(issue)?.ToString() ?? "No description"; + var location = t.GetProperty("Location")?.GetValue(issue); + string filePath = GetAuditorIssuePath(location); + + if (ShouldSkipAuditorIssue(category, description, filePath, isSandbox, targetPath, sandboxIgnorePatterns)) + { + return; + } + + var i = new JObject { ["category"] = category, ["description"] = description, ["file"] = filePath }; + if (location != null) + { + var locType = location.GetType(); + i["line"] = locType.GetProperty("Line")?.GetValue(location)?.ToString(); + } + + codeIssues.Add(i); + } + + private static string GetAuditorIssuePath(object location) + { + return location?.GetType().GetProperty("Path")?.GetValue(location)?.ToString() ?? ""; + } + + private static bool ShouldSkipAuditorIssue(string category, string description, string filePath, bool isSandbox, string targetPath, string[] sandboxIgnorePatterns) + { + if (category.Contains("Code")) + { + if (string.IsNullOrEmpty(filePath) || !filePath.StartsWith(targetPath)) + { + return true; + } + + return isSandbox && sandboxIgnorePatterns.Any(pattern => description.IndexOf(pattern, StringComparison.OrdinalIgnoreCase) >= 0); + } + + return isSandbox && (string.IsNullOrEmpty(filePath) || (!filePath.Contains("com.forkhorizon.nexus.unity") && !filePath.Contains("Assets/NexusUnity"))); + } + + private static void RunNexusStyleAudit(JObject result) + { + string customTargetPath = Directory.Exists("Assets/NexusUnity") ? "Assets/NexusUnity" : "Assets"; + var codeIssuesList = result["code_issues"] as JArray ?? new JArray(); + NexusEditorLog.Log(NexusLogCategory.Audit, $"[Nexus Style Audit] Scanning path: {customTargetPath}, current issues: {codeIssuesList.Count}"); + + string[] files = Directory.GetFiles(customTargetPath, "*.cs", SearchOption.AllDirectories); + int styleIssuesAdded = 0; + foreach (var file in files) + { + string relativePath = file.Replace(Directory.GetCurrentDirectory() + "/", "").Replace("\\", "/"); + if (relativePath.Contains("Assets/")) relativePath = relativePath.Substring(relativePath.IndexOf("Assets/")); + + string[] lines = File.ReadAllLines(file); + if (lines.Length <= 300) + { + continue; + } + + codeIssuesList.Add(new JObject { ["category"] = "Style", ["description"] = $"File exceeds 300 lines limit (Current: {lines.Length} lines).", ["file"] = relativePath, ["line"] = "1" }); + styleIssuesAdded++; + } + + result["code_issues"] = codeIssuesList; + result["num_total_issues"] = codeIssuesList.Count; + NexusEditorLog.Log(NexusLogCategory.Audit, $"[Nexus Style Audit] Added {styleIssuesAdded} style issues. Total: {codeIssuesList.Count}", true); + } + private static void ScanSceneHealth(JArray issues) { var allGOs = Resources.FindObjectsOfTypeAll() diff --git a/README.md b/README.md index e56e49b..953a352 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,8 @@ For reproducible installs, pin the public release tag: https://github.com/ForkHorizon/NexusUnity.git#v1.4.1 ``` +Nexus Unity does not declare Unity Project Auditor packages. Its lint tool always runs Nexus style and scene checks, and only includes Unity Project Auditor findings when the host project explicitly has compatible Project Auditor rules installed. + ## Start The Server 1. Open `Window > Nexus Unity`. diff --git a/package.json b/package.json index 0c1e133..e05102e 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,6 @@ }, "dependencies": { "com.unity.inputsystem": "1.19.0", - "com.unity.nuget.newtonsoft-json": "2.0.0", - "com.unity.project-auditor": "0.10.0-preview.1", - "com.unity.project-auditor-rules": "1.0.2" + "com.unity.nuget.newtonsoft-json": "2.0.0" } } diff --git a/scripts/prepush-validate.sh b/scripts/prepush-validate.sh index ab983b7..57a23c2 100755 --- a/scripts/prepush-validate.sh +++ b/scripts/prepush-validate.sh @@ -34,6 +34,14 @@ assert package["name"] == "com.forkhorizon.nexus.unity" assert package["version"] assert package["license"] == "GPL-3.0-only" assert package["repository"]["url"] == "https://github.com/ForkHorizon/NexusUnity.git" + +dependencies = package.get("dependencies", {}) +for package_name in ("com.unity.project-auditor", "com.unity.project-auditor-rules"): + assert package_name not in dependencies, ( + f"Do not depend on {package_name}; Nexus audit support uses reflection " + "when a host project explicitly installs compatible Unity Project " + "Auditor packages." + ) PY log "Compiling Python bridge"