diff --git a/Aspid.FastTools/Assets/DevTests/SerializeReferences.meta b/Aspid.FastTools/Assets/DevTests/SerializeReferences.meta
new file mode 100644
index 00000000..0ff0eb60
--- /dev/null
+++ b/Aspid.FastTools/Assets/DevTests/SerializeReferences.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: ccb39851c31fa412387abddb80e2184c
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/DevTests/SerializeReferences/Prefabs.meta b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Prefabs.meta
new file mode 100644
index 00000000..1176fb9d
--- /dev/null
+++ b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Prefabs.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 962a4b4aa23de4357a3e5641c90a1ed3
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/DevTests/SerializeReferences/Prefabs/RequiredViolationsDevTest.prefab b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Prefabs/RequiredViolationsDevTest.prefab
new file mode 100644
index 00000000..ae24ea5c
--- /dev/null
+++ b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Prefabs/RequiredViolationsDevTest.prefab
@@ -0,0 +1,54 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!1 &6785315564667727353
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 2201127709315222250}
+ - component: {fileID: 6873221168316818700}
+ m_Layer: 0
+ m_Name: RequiredViolationsDevTest
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &2201127709315222250
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 6785315564667727353}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!114 &6873221168316818700
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 6785315564667727353}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 9600738624fa14a36ab7f3cc1c063e27, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::Aspid.FastTools.DevTests.SerializeReferences.RequiredViolationsDevTest
+ _requiredReference:
+ rid: -2
+ _requiredTypeName:
+ references:
+ version: 2
+ RefIds:
+ - rid: -2
+ type: {class: , ns: , asm: }
diff --git a/Aspid.FastTools/Assets/DevTests/SerializeReferences/Prefabs/RequiredViolationsDevTest.prefab.meta b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Prefabs/RequiredViolationsDevTest.prefab.meta
new file mode 100644
index 00000000..7c7eecdc
--- /dev/null
+++ b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Prefabs/RequiredViolationsDevTest.prefab.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: d1ffcb56686504f69ae8e81a82a51deb
+PrefabImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts.meta b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts.meta
new file mode 100644
index 00000000..17d7b63d
--- /dev/null
+++ b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 982b58ec7bfff43e1b2ae1ccdad92cde
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/RequiredViolationsDevTest.cs b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/RequiredViolationsDevTest.cs
new file mode 100644
index 00000000..1d44dec9
--- /dev/null
+++ b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/RequiredViolationsDevTest.cs
@@ -0,0 +1,26 @@
+using System;
+using UnityEngine;
+using Aspid.FastTools.Types;
+
+// ReSharper disable once CheckNamespace
+namespace Aspid.FastTools.DevTests.SerializeReferences
+{
+ public interface IRequiredDevTestPayload { }
+
+ [Serializable]
+ public sealed class RequiredDevTestPayload : IRequiredDevTestPayload
+ {
+ [SerializeField] private int _value;
+ }
+
+ // Dev-only fixture for manually verifying the Project References "Required violations" group and the Asset
+ // References REQUIRED badge: both fields are left unset on Prefabs/RequiredViolationsDevTest.prefab on purpose.
+ public sealed class RequiredViolationsDevTest : MonoBehaviour
+ {
+ [SerializeReference, TypeSelector(Required = true)]
+ private IRequiredDevTestPayload _requiredReference;
+
+ [TypeSelector(typeof(Component), Required = true)]
+ [SerializeField] private string _requiredTypeName;
+ }
+}
diff --git a/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/RequiredViolationsDevTest.cs.meta b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/RequiredViolationsDevTest.cs.meta
new file mode 100644
index 00000000..4362e510
--- /dev/null
+++ b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/RequiredViolationsDevTest.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 9600738624fa14a36ab7f3cc1c063e27
\ No newline at end of file
diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Tests/Editor/SerializeReferences/SerializeReferenceGateScannerTests.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Tests/Editor/SerializeReferences/SerializeReferenceGateScannerTests.cs
index 814dacdf..12fbdd3d 100644
--- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Tests/Editor/SerializeReferences/SerializeReferenceGateScannerTests.cs
+++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Tests/Editor/SerializeReferences/SerializeReferenceGateScannerTests.cs
@@ -1,14 +1,110 @@
+using System.Linq;
+using UnityEditor;
+using UnityEngine;
using NUnit.Framework;
+using System.Text.RegularExpressions;
namespace Aspid.FastTools.SerializeReferences.Editors.Tests
{
///
/// Coverage for — the gate's pre-filter that keeps
- /// properly declared renames from ever warning or failing a build / CI run.
+ /// properly declared renames from ever warning or failing a build / CI run — and for
+ /// itself surfacing unset required fields (the data source for the Project References "Required violations" group).
///
[TestFixture]
internal sealed class SerializeReferenceGateScannerTests
{
+ private const string ProbeAssetPath = "Assets/__AspidGateScannerRequiredProbe__.asset";
+
+ // Scan(RequiredOnly) is the exact call the Project References "Required violations" group makes; this proves
+ // it surfaces both an unset managed reference and an unset [TypeSelector(Required = true)] string field on a
+ // saved asset (RequiredTestObject, shared fixture — see SerializeReferenceTestFixtures.cs).
+ [Test]
+ public void Scan_RequiredOnly_SurfacesUnsetRequiredFieldsOnSavedAsset()
+ {
+ var probe = ScriptableObject.CreateInstance();
+ try
+ {
+ AssetDatabase.CreateAsset(probe, ProbeAssetPath);
+ AssetDatabase.TryGetGUIDAndLocalFileIdentifier(probe, out _, out long fileId);
+
+ var violations = SerializeReferenceGateScanner.Scan(GateOptions.RequiredOnly);
+ var forProbe = violations.Where(v => v.AssetPath == ProbeAssetPath && v.FileId == fileId).ToList();
+
+ Assert.IsTrue(forProbe.All(v => v.Kind == GateViolationKind.RequiredUnset));
+ Assert.IsTrue(forProbe.Any(v => v.FieldPath == nameof(RequiredTestObject.requiredRef)),
+ "Unset [SerializeReference, TypeSelector(Required = true)] field must be reported.");
+ Assert.IsTrue(forProbe.Any(v => v.FieldPath == nameof(RequiredTestObject.requiredString)),
+ "Unset [TypeSelector(Required = true)] string field must be reported.");
+ }
+ finally
+ {
+ AssetDatabase.DeleteAsset(ProbeAssetPath);
+ }
+ }
+
+ // ScanAssetRequiredFields is the scoped, single-asset entry point the Inspect Asset graph calls on every
+ // Rescan; it must agree with Scan(RequiredOnly) for that one asset without sweeping the whole project.
+ [Test]
+ public void ScanAssetRequiredFields_SavedAsset_MatchesProjectScan()
+ {
+ var probe = ScriptableObject.CreateInstance();
+ try
+ {
+ AssetDatabase.CreateAsset(probe, ProbeAssetPath);
+ AssetDatabase.TryGetGUIDAndLocalFileIdentifier(probe, out _, out long fileId);
+
+ var violations = SerializeReferenceGateScanner.ScanAssetRequiredFields(ProbeAssetPath);
+ var forProbe = violations.Where(v => v.FileId == fileId).ToList();
+
+ Assert.IsTrue(forProbe.All(v => v.Kind == GateViolationKind.RequiredUnset));
+ Assert.IsTrue(forProbe.Any(v => v.FieldPath == nameof(RequiredTestObject.requiredRef)),
+ "Unset [SerializeReference, TypeSelector(Required = true)] field must be reported.");
+ Assert.IsTrue(forProbe.Any(v => v.FieldPath == nameof(RequiredTestObject.requiredString)),
+ "Unset [TypeSelector(Required = true)] string field must be reported.");
+ }
+ finally
+ {
+ AssetDatabase.DeleteAsset(ProbeAssetPath);
+ }
+ }
+
+ [Test]
+ public void ScanAssetRequiredFields_NonCandidatePath_ReturnsEmpty()
+ {
+ Assert.AreEqual(0, SerializeReferenceGateScanner.ScanAssetRequiredFields("Assets/Fake.txt").Count);
+ }
+
+ // The Inspect Asset graph (SerializeReferenceGraphView) badges an empty [SerializeReference] slot as REQUIRED
+ // by matching its graph field path — built independently by SerializeReferenceGraphScanner straight from
+ // YAML — against this scan's GateViolation.FieldPath (a live SerializedProperty.propertyPath), after the same
+ // "[i]" -> ".Array.data[i]" normalization the view applies. This proves the two paths actually agree for a
+ // real saved asset, not just that each individually finds the field.
+ [Test]
+ public void ScanAssetRequiredFields_UnsetManagedReference_MatchesGraphScannerEmptyRootPath()
+ {
+ var probe = ScriptableObject.CreateInstance();
+ try
+ {
+ AssetDatabase.CreateAsset(probe, ProbeAssetPath);
+ AssetDatabase.TryGetGUIDAndLocalFileIdentifier(probe, out _, out long fileId);
+
+ var violations = SerializeReferenceGateScanner.ScanAssetRequiredFields(ProbeAssetPath);
+ var document = SerializeReferenceGraphScanner.Build(ProbeAssetPath).Single(doc => doc.FileId == fileId);
+ var emptyRoot = document.Roots.Single(root => root.IsEmpty);
+
+ var normalizedGraphPath = Regex.Replace(emptyRoot.Label, @"\[(\d+)\]", ".Array.data[$1]");
+
+ Assert.IsTrue(
+ violations.Any(v => v.FileId == fileId && v.FieldPath == normalizedGraphPath),
+ $"Normalized graph path '{normalizedGraphPath}' must match a ScanAssetRequiredFields violation's FieldPath.");
+ }
+ finally
+ {
+ AssetDatabase.DeleteAsset(ProbeAssetPath);
+ }
+ }
+
// A [MovedFrom]-claimed stale name is a pending migration, not a violation: Unity migrates the reference in
// memory at load, so the gate must accept it — a properly declared rename can never warn or fail a build /
// CI run. A scene path exercises the trust-the-claim branch (constraints are unrecoverable for scenes).
diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Resources/UI/SerializeReferences/Aspid-FastTools-ReferenceGraph.uss b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Resources/UI/SerializeReferences/Aspid-FastTools-ReferenceGraph.uss
index 815b7e57..f1992cb3 100644
--- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Resources/UI/SerializeReferences/Aspid-FastTools-ReferenceGraph.uss
+++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Resources/UI/SerializeReferences/Aspid-FastTools-ReferenceGraph.uss
@@ -475,6 +475,7 @@
-unity-text-align: middle-center;
}
+
/* --- Inline type picker (mirrors the Project Audit picker exactly) ---
The selector view expanded inline, welded into the card being fixed (see TogglePicker): translucent dark fill,
neutral border, 8px radius and a fixed height (the ListView needs bounds to virtualize). padding:0 plus the header /
diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Resources/UI/SerializeReferences/Aspid-FastTools-SerializeReference.uss b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Resources/UI/SerializeReferences/Aspid-FastTools-SerializeReference.uss
index bf060ea4..24f9438c 100644
--- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Resources/UI/SerializeReferences/Aspid-FastTools-SerializeReference.uss
+++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Resources/UI/SerializeReferences/Aspid-FastTools-SerializeReference.uss
@@ -586,6 +586,14 @@
align-items: center;
}
+/* The Required-violations card has no Fix all button hosting its header row, so the static row carries the same
+ 4px horizontal padding and 6px gap the button gives the broken-type cards — keeping every card's header and
+ entry paths on one shared left edge. */
+.aspid-fasttools-repair-references__group-header-row--static {
+ padding: 4px 4px;
+ margin-bottom: 6px;
+}
+
.aspid-fasttools-repair-references__group-header {
flex-shrink: 1;
}
@@ -664,6 +672,15 @@
-unity-text-align: middle-right;
}
+/* A required-violation row's "Component.field" column — same dim, right-pinned treatment as __group-entry-rid, one
+ column over for a different row shape (Required violations card has no rid). */
+.aspid-fasttools-repair-references__group-entry-field {
+ flex-shrink: 0;
+ margin-left: 10px;
+ color: var(--aspid-colors-text-dark);
+ -unity-text-align: middle-right;
+}
+
/* A compatible MonoScript dragged over the field highlights the header as a valid drop target. */
.aspid-fasttools-serialize-reference--drop-target {
background-color: var(--aspid-colors-bg-light);
diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Build/GateOptions.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Build/GateOptions.cs
index 70eafe50..7cb8f5d5 100644
--- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Build/GateOptions.cs
+++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Build/GateOptions.cs
@@ -15,6 +15,9 @@ internal readonly struct GateOptions
public static GateOptions MissingOnly =>
new(true, false);
+ public static GateOptions RequiredOnly =>
+ new(false, true);
+
private GateOptions(bool scanMissingTypes, bool scanRequiredFields)
{
ScanMissingTypes = scanMissingTypes;
diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Build/SerializeReferenceGateScanner.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Build/SerializeReferenceGateScanner.cs
index 952eb5b5..9d520143 100644
--- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Build/SerializeReferenceGateScanner.cs
+++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Build/SerializeReferenceGateScanner.cs
@@ -57,6 +57,27 @@ public static IReadOnlyList Scan(GateOptions options, Action
+ /// Scoped required-field scan for a single asset, without a full project sweep.
+ ///
+ ///
+ /// Reuses 's per-path dispatch for . Used by the
+ /// Inspect Asset graph, which needs one asset's violations on every Rescan; skipped for a path that is not
+ /// itself a scan candidate, matching every other consumer's .
+ ///
+ public static IReadOnlyList ScanAssetRequiredFields(string assetPath)
+ {
+ var violations = new List();
+ if (string.IsNullOrEmpty(assetPath) || !SerializeReferenceHelpers.IsScanCandidate(assetPath)) return violations;
+
+ ScriptRequiredFieldsCache.Clear();
+
+ if (SerializeReferenceHelpers.IsScene(assetPath)) CollectSceneRequiredViolations(assetPath, violations);
+ else CollectRequiredViolations(assetPath, violations);
+
+ return violations;
+ }
+
// A stored name claimed by exactly one declared [MovedFrom] is a pending migration, not a violation —
// Unity migrates it in memory at load — provided the target still fits the field's declared type.
// Scenes cannot be object-loaded to recover constraints, so a scene entry claimed by a rename is trusted.
diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Windows/SerializeReferenceGraphView.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Windows/SerializeReferenceGraphView.cs
index 3a7ea9b8..7b772987 100644
--- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Windows/SerializeReferenceGraphView.cs
+++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Windows/SerializeReferenceGraphView.cs
@@ -93,6 +93,9 @@ internal sealed class SerializeReferenceGraphView : VisualElement
private const string ChangeCollapsedText = "Change ▼";
private const string AssignCollapsedText = "Assign ▼";
+ // A required slot's band verb names what the amber is about: the field must be assigned, not merely can be.
+ private const string AssignRequiredCollapsedText = "Assign Required ▼";
+
// A pending-migration card is not missing (Unity migrates it in memory; only the file is stale), so no "Missing".
private const string MigrateFixCollapsedText = "Fix ▼";
private const char BandChevronCollapsed = '▼';
@@ -127,6 +130,13 @@ internal sealed class SerializeReferenceGraphView : VisualElement
// so each Fix-Missing picker open must not re-scan. Cleared on every Rescan / apply so rewritten YAML is re-read.
private readonly Dictionary> _constraintCache = new(StringComparer.Ordinal);
+ // Unset [TypeSelector(Required = true)] fields for the current asset, refreshed on every Rescan. Populated
+ // straight from SerializeReferenceGateScanner — the same required-field check the Project References audit
+ // and the build/CI gate use — so the amber required styling here always agrees with them. A required
+ // string/SerializableType field has no rid and so no graph node; it gets its own trailing card instead
+ // (BuildRequiredOnlyCard).
+ private IReadOnlyList _requiredViolations = Array.Empty();
+
public SerializeReferenceGraphView(Object target, Action onCanvasTone, Action