diff --git a/docs/ISSUES.md b/docs/ISSUES.md
index 9da6482f..b38196dd 100644
--- a/docs/ISSUES.md
+++ b/docs/ISSUES.md
@@ -10738,7 +10738,7 @@ user descended the spiral (`retail-spiral2.log`, 767 samples). **Retail's flood
and IDENTICAL in character to ours** — from the spiral cells it swings `num` 3→27 with gaze
and COLLAPSES to 3 cells at many poses (e.g. cam=015d eye(60.05,-28.51,-3.66) → just
{015d 015e 015f}; cam=014b → {014b 014c 014a}). Our flood does the same (3→43, headless
-`FloodDepthFrom015E_VsRetail26` = max 43 vs retail 26). So retail does NOT keep the spiral
+`Diagnostic_FloodDepthFrom015E_VsRetail26` = max 43 vs retail 26). So retail does NOT keep the spiral
where we drop it — the flood is EXONERATED as the cause. **The vanish must be DOWNSTREAM of
the flood** (unexplored): the steps in these spiral cells are STATIC objects (GfxObj
`0x010000DE` ×6/cell), drawn via the separate viewcone cull (`ViewconeCuller.SphereVisibleInCell`
diff --git a/docs/plans/2026-06-11-building-render-port-plan.md b/docs/plans/2026-06-11-building-render-port-plan.md
index f367e32a..a305d62c 100644
--- a/docs/plans/2026-06-11-building-render-port-plan.md
+++ b/docs/plans/2026-06-11-building-render-port-plan.md
@@ -104,7 +104,7 @@ inline when ported.
### BR-1 — The surface gate — ✅ RESOLVED AS ALREADY-EQUIVALENT (2026-06-11, execution day 1)
**Premise falsified before implementation (the BR-1 pre-check,
-`ReplicateProductionEmission_OnPortalFills`):** acdream **already suppresses
+`Diagnostic_ReplicateProductionEmission_OnPortalFills`):** acdream **already suppresses
every portal fill** — all four extraction paths skip `Stippling.NoPos`
positive sides (`ObjectMeshManager.PrepareGfxObjMeshData:1046`,
`PrepareCellStructMeshData:1394`, `CellMesh.Build:44`, `GfxObjMesh.Build:71`),
diff --git a/docs/release-gate.md b/docs/release-gate.md
index b7cc8f4b..35c3cbcd 100644
--- a/docs/release-gate.md
+++ b/docs/release-gate.md
@@ -99,6 +99,16 @@ expected until the linked defect is fixed. Diagnostic apparatus
(`Purpose=Diagnostic`) likewise reports separately and does not inflate the
contract-test pass count.
+The current diagnostic apparatus lives in App and Core. It is retained for
+investigation output, and several methods require installed DATs:
+
+```powershell
+dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj -c Release `
+ --filter 'Purpose=Diagnostic'
+dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj -c Release `
+ --filter 'Purpose=Diagnostic'
+```
+
## Updating dependencies
Do not edit lock files by hand. To make an intentional dependency change:
diff --git a/docs/research/2026-06-11-building-render-acdream-vs-retail-comparison.md b/docs/research/2026-06-11-building-render-acdream-vs-retail-comparison.md
index 2e5011b1..cf7617a8 100644
--- a/docs/research/2026-06-11-building-render-acdream-vs-retail-comparison.md
+++ b/docs/research/2026-06-11-building-render-acdream-vs-retail-comparison.md
@@ -371,7 +371,7 @@ area files.
> (`ObjectMeshManager.PrepareGfxObjMeshData:1046`,
> `PrepareCellStructMeshData:1394`, `CellMesh.Build:44`,
> `GfxObjMesh.Build:71`), and the fills have no negative surface
-> (`ReplicateProductionEmission_OnPortalFills`: pos=False/neg=False for every
+> (`Diagnostic_ReplicateProductionEmission_OnPortalFills`: pos=False/neg=False for every
> fill). The equivalence pin (`StipplingSurfaceEquivalenceTests`, 2,607
> polys, 0 violations) proves our build-time skip ⇔ retail's draw-time
> `skipNoTexture` on this content. Consequences: the ledger rows
diff --git a/docs/research/2026-07-06-176-177-handoff-A7-lighting.md b/docs/research/2026-07-06-176-177-handoff-A7-lighting.md
index 54dceac5..47ab117f 100644
--- a/docs/research/2026-07-06-176-177-handoff-A7-lighting.md
+++ b/docs/research/2026-07-06-176-177-handoff-A7-lighting.md
@@ -83,7 +83,7 @@ draw the purple lightning over the floor"):
3. **Striped floor z-fight-like artifact.** User's 2nd screenshot: regular
magenta bands across one floor region, "like something is fighting to draw
the purple over the floor." **NOT attributed.** Ruled out: not coincident dat
- geometry (the `CorridorNeighborhood_CoplanarOverlappingDrawnPolyPairs` sweep
+ geometry (the `Diagnostic_CorridorNeighborhood_CoplanarOverlappingDrawnPolyPairs` sweep
found only the legit z=−12 under-hall floor quad-fan, nothing near the −6
corridor floor); not a striped texture (all corridor surfaces are plain
`Base1Image` stone 0x08000375/6/7/8). Leading guess: two draws of the same
@@ -116,7 +116,7 @@ draw the purple lightning over the floor"):
stair geometry owner (`0x8A020182`'s ramp shell, vertical portals, ZERO
statics), CellBSP containment (partitions exactly at portal planes),
under-hall + corridor drawn-poly surface colors, DXT1 alpha histograms (0
- transparent texels), and `CorridorNeighborhood_CoplanarOverlappingDrawnPolyPairs`
+ transparent texels), and `Diagnostic_CorridorNeighborhood_CoplanarOverlappingDrawnPolyPairs`
(the stripe-geometry sweep — came back empty for the −6 floor).
- **`tests/AcDream.App.Tests/Rendering/Issue176177FacilityHubFloodReplayTests.cs`**
— production-matched portal-flood replays (approach/descent/gaze-sweep/walk +
diff --git a/docs/research/2026-07-06-176-seam-floor-zfight-handoff.md b/docs/research/2026-07-06-176-seam-floor-zfight-handoff.md
index d9d84533..9aa5faf0 100644
--- a/docs/research/2026-07-06-176-seam-floor-zfight-handoff.md
+++ b/docs/research/2026-07-06-176-seam-floor-zfight-handoff.md
@@ -48,7 +48,7 @@ NOT throw more lighting fixes at it (two already did nothing).
| Lighting **selection** | `SelectForCell` (all dynamic lights per cell, retail-exact) → **no visual change** |
| Light-set **camera-cap churn** (the OLD "confirmed" #176 theory) | visible-cell scoping shipped (`c500912b`); probe-proven ~285 through-floor lights dropped/frame — symptom unchanged. **REFUTED.** |
| **Membership / the "flap"** | `ACDREAM_PROBE_FLAP`: render cell = `0x8A020164` on **100%** of 188,732 frames across **526 distinct camera angles**, `res=None` always; `ACDREAM_PROBE_PVINPUT` flood is stable-per-angle (17/10/8…), never oscillates at a fixed view; 1 `[cell-transit]` (the spawn teleport) total |
-| **Dat-geometry z-fight** | `Issue176177DungeonSeamInspectionTests.CorridorNeighborhood_CoplanarOverlappingDrawnPolyPairs` seeded on the ACTUAL cell `0x8A020164` + neighbors → **zero** coplanar drawn pairs at the z=−6 corridor floor (only the benign same-cell z=−12 under-hall floor tiling in `0x011E`) |
+| **Dat-geometry z-fight** | `Issue176177DungeonSeamInspectionTests.Diagnostic_CorridorNeighborhood_CoplanarOverlappingDrawnPolyPairs` seeded on the ACTUAL cell `0x8A020164` + neighbors → **zero** coplanar drawn pairs at the z=−6 corridor floor (only the benign same-cell z=−12 under-hall floor tiling in `0x011E`) |
| A2C / alpha-hole see-through | corridor floor surface `0x08000377` is **fully opaque** (`alpha0Texels=0`, `transl=0.00`) |
| Translucent under-surface blend | the one translucent colored surface `0x08000034` is `NoPos` (not drawn) |
| Flat (per-face) normals | corridor floor uses **smooth per-vertex dat normals** (center `(0,0,1)`, corners tilted ~27° — retail-style edge smoothing), NOT flat (`CellVertexNormals_SmoothOrFaceted_Dump`) |
@@ -129,7 +129,7 @@ RenderDoc (do NOT assume — capture and read):
- `ACDREAM_PROBE_INDOOR_LIGHT=1` — `[indoor-light]` scoped-pool SET composition.
- `tools/cdb/issue176-floor-light.cdb` — retail light-setup trace.
- `Issue176177DungeonSeamInspectionTests` — dat truth (coplanar sweep, floor
- surfaces, vertex normals); `CorridorNeighborhood_CoplanarOverlappingDrawnPolyPairs`
+ surfaces, vertex normals); `Diagnostic_CorridorNeighborhood_CoplanarOverlappingDrawnPolyPairs`
seed = `{0164,0165,016E,017A}`+neighbors.
## Repro + launch protocol
diff --git a/docs/reviews/2026-08-18-r3-test-truth-ledger.md b/docs/reviews/2026-08-18-r3-test-truth-ledger.md
index 348e785a..107704ee 100644
--- a/docs/reviews/2026-08-18-r3-test-truth-ledger.md
+++ b/docs/reviews/2026-08-18-r3-test-truth-ledger.md
@@ -26,6 +26,50 @@ The six-case reduction from the earlier 14,748-pass headline is understood:
seven launcher editor-focus theory rows were consolidated into one fact that
still executes all seven variants in a single Avalonia application session.
+## Fresh reproducible syntax inventory
+
+`tools/audit-test-inventory.ps1` now regenerates the complete attributed-method
+manifest from tracked C# test sources with Roslyn:
+
+```powershell
+pwsh ./tools/audit-test-inventory.ps1
+```
+
+The generated JSON lives under ignored `artifacts/test-audit/`; it is not a
+second 6.8 MB checked-in source of truth. The script and this reviewed ledger
+are durable, while paths and line numbers regenerate after every batch. The
+inventory at commit `8f490240` reports:
+
+| Syntax signal | Current count |
+|---|---:|
+| Tracked/parsed C# test files | 1,256 / 1,256 |
+| Attributed test methods (not expanded theory cases) | 11,455 |
+| Methods containing at least one empty `return;` | 399 |
+| Empty-return sites | 426 |
+| Prerequisite-return candidates | 282 |
+| Mechanical output-only candidates | 47 |
+| Methods directly using `Thread.Sleep` / `Task.Delay` | 20 / 23 |
+| Methods directly reading environment variables | 47 |
+| Methods directly reading `.cs` source text | 63 |
+
+The 282 prerequisite-return count is intentionally a review candidate set, not
+a claim that all 399 methods are broken. It comprises 109 App, 125 Core, 29
+Content, 12 Launcher.Core, three Core.Net, three Bake, and one Headless method.
+The classifier includes external-asset, opt-in, and platform gates; behavioral
+early exits inside callback tests remain outside that candidate set. Every
+candidate still requires source inspection because `Directory.Exists`, for
+example, can also be the behavior under test.
+
+The output-only classifier follows same-file helper calls, but it cannot decide
+whether an assertion merely establishes probe setup. The reviewed diagnostic
+set is therefore 51 methods: the 47 mechanical candidates plus
+`OptionsPanelLiveMountProbeTests.ProbeLiveMountShapes`,
+`DoorSetupGfxObjInspectionTests.HoltburgCottage_CellPortals_DatInspection`, and
+the two already classified tower diagnostics. Relative to T-011's historical
+51-method table, delete the removed PVS scaffold and add
+`TowerAscentReplayTests.Diagnostic_TowerAscent_PerStepTable`; all other entries
+remain. Those 51 methods expand to 70 discovered cases (28 App, 42 Core).
+
## Exact 77-skip reconciliation
The latest clean-gate TRX files account for every reported skip:
@@ -83,11 +127,11 @@ identifiable in the lane report.
| T-004 warning mismatches | resolved in R2 | Clean rebuild is zero-warning with repository-wide warnings-as-errors. |
| T-005 unreachable panel stack | requires production reachability proof | Do not delete 52 meaningful tests until the dead presentation surface and any supported plugin compatibility promise are verified together. |
| T-006 misleading installed-DAT reason | resolved in batch B | Shared opt-in is now `ACDREAM_RUN_INSTALLED_DAT_TESTS=1` (legacy switch retained), the reason names the lane, and all nine owners carry `Lane=InstalledDat`. |
-| T-007 271 silent passing gates | open | Re-run the syntax inventory against current source, then convert each to a hermetic fixture, explicit prerequisite skip, or non-default lane. |
+| T-007 271 silent passing gates | active; current candidate set generated | Current syntax has 282 prerequisite-return candidates across seven projects. Inspect/classify from the generated manifest; the count changed with source growth and includes platform gates, so do not mechanically rewrite all 282. |
| T-008 incapable-of-failing diagnostics | batch A plus diagnostic lane work | Delete the literal wiring smoke test; repair the GPU contract tautology; later re-home output-only apparatus. |
| T-009 wall-clock double-click tests | open | Introduce a behavior-preserving injectable monotonic clock and deterministic fake time. |
| T-010 two useless cases | high-confidence cleanup batch A | Delete `SmokeTest.TestProject_IsWired` and `ChaseCameraTests.ImplementsICamera`; compilation already proves both claims. |
-| T-011 51 output-only methods | open | Validate the historical 51-method list against current source; add stable oracles or move them outside default discovery. |
+| T-011 51 output-only methods | classified in batch C | The reviewed current set is 51 methods / 70 cases. All carry `Purpose=Diagnostic`, preserving the apparatus while removing it from release pass totals. Contract-shaped names remain explicitly flagged until a stable oracle exists. |
| T-012 source-text freezes | requires semantic replacement map | Retain whole-tree dependency rules; remove exact-text freezes only when an equivalent semantic/behavioral guard is identified. |
| T-013 controller self-comparison | high-confidence cleanup batch A | Capture the first controller next to the first body and compare every retry with that reference. |
| T-014 seven load-sensitive tests | active; first mechanism reproduced | `PortalProjectionTests.ClipToRegion_FrameOwnedStore_ReusesExactResultArray` reported 6,904 B only while three App test hosts ran concurrently, then passed in an isolated run. Preserve both observations and remove ambient process-allocation sensitivity rather than loosening the zero-allocation oracle. |
@@ -179,3 +223,31 @@ at 6,904 bytes while the Installed-DAT and discovery hosts ran beside it. The
same unmodified hermetic command then passed 5,505/5,505 in isolation. This is
evidence for removing cross-host allocation sensitivity; it is not grounds to
weaken the assertion or claim a clean concurrent result.
+
+## Batch C diagnostic classification
+
+Batch C assigns the reviewed 51-method diagnostic set to
+`Purpose=Diagnostic`. No body, output, historical comment, or fixture is
+removed. This resolves the misleading release accounting without pretending
+that every investigation has a stable regression oracle.
+
+The 15 contract-shaped methods called out by T-011 now have a `Diagnostic_`
+prefix: four facility-hub scenarios, six stair/camera sweeps, two ambient-slot
+inventories, production-emission replication, and two drawn-polygon reports.
+Their current output and historical method references are preserved, while the
+new names no longer imply an asserted oracle. Promoting them back into the
+hermetic lane requires an asserted expected result; R3 will not invent those
+values.
+
+Batch C verification:
+
+- App Release build: 0 warnings, 0 errors;
+- Core Release build, serialized after App: 0 warnings, 0 errors;
+- hermetic App: 5,479 passed / 0 skipped / 0 failed;
+- hermetic Core: 4,753 passed / 0 skipped / 0 failed; and
+- diagnostic discovery: 28 App + 42 Core = 70 cases from 51 methods.
+
+An attempted parallel App/Core build collided on the shared
+`AcDream.Plugin.Abstractions` intermediate DLL. The repository gate serializes
+projects, so the authoritative verification is the two isolated clean builds
+above; no code or assertion was changed in response to that tooling collision.
diff --git a/tests/AcDream.App.Tests/Rendering/CornerFloodReplayTests.cs b/tests/AcDream.App.Tests/Rendering/CornerFloodReplayTests.cs
index 351b356b..74da2f81 100644
--- a/tests/AcDream.App.Tests/Rendering/CornerFloodReplayTests.cs
+++ b/tests/AcDream.App.Tests/Rendering/CornerFloodReplayTests.cs
@@ -541,6 +541,7 @@ public class CornerFloodReplayTests
/// Scratch: the homogeneous reciprocal primitive in isolation, on the
/// synthetic fixture geometry that Build_AppliesReciprocalOtherPortalClip uses.
[Fact]
+ [Trait("Purpose", "Diagnostic")]
public void Scratch_ReciprocalPrimitive_SyntheticPair()
{
var view = Matrix4x4.CreateLookAt(Vector3.Zero, new Vector3(0, 0, -1), Vector3.UnitY);
diff --git a/tests/AcDream.App.Tests/Rendering/Issue131SetupProbeTests.cs b/tests/AcDream.App.Tests/Rendering/Issue131SetupProbeTests.cs
index 0c60c71a..808c2ab1 100644
--- a/tests/AcDream.App.Tests/Rendering/Issue131SetupProbeTests.cs
+++ b/tests/AcDream.App.Tests/Rendering/Issue131SetupProbeTests.cs
@@ -14,6 +14,7 @@ namespace AcDream.App.Tests.Rendering;
/// candidate setup's parts + bounds from the dat. The portal's setup is the
/// translucent swirl; lamp posts / creatures / signs identify by part shape.
///
+[Trait("Purpose", "Diagnostic")]
public class Issue131SetupProbeTests
{
private readonly ITestOutputHelper _out;
diff --git a/tests/AcDream.App.Tests/Rendering/Issue176177FacilityHubFloodReplayTests.cs b/tests/AcDream.App.Tests/Rendering/Issue176177FacilityHubFloodReplayTests.cs
index 0bc52627..e95dc97b 100644
--- a/tests/AcDream.App.Tests/Rendering/Issue176177FacilityHubFloodReplayTests.cs
+++ b/tests/AcDream.App.Tests/Rendering/Issue176177FacilityHubFloodReplayTests.cs
@@ -98,7 +98,8 @@ public class Issue176177FacilityHubFloodReplayTests
}
[Fact]
- public void ScenarioB_StairDescent_RampCellRetention()
+ [Trait("Purpose", "Diagnostic")]
+ public void Diagnostic_ScenarioB_StairDescent_RampCellRetention()
{
var datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; }
@@ -140,7 +141,8 @@ public class Issue176177FacilityHubFloodReplayTests
}
[Fact]
- public void ScenarioC_CorridorSeamGazeSweep_Bistability()
+ [Trait("Purpose", "Diagnostic")]
+ public void Diagnostic_ScenarioC_CorridorSeamGazeSweep_Bistability()
{
var datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; }
@@ -212,9 +214,10 @@ public class Issue176177FacilityHubFloodReplayTests
/// #177(a)/(c) at the stair transit.
///
[Theory]
+ [Trait("Purpose", "Diagnostic")]
[InlineData(0x016Eu, 0x017Au, 85.00f, -4.3f)] // corridor seam, plane x=85
[InlineData(0x0182u, 0x0183u, 98.333f, -7.3f)] // stair-bottom transit, plane x=98.33
- public void ScenarioE_RootLagWindow_ForwardChainRetention(
+ public void Diagnostic_ScenarioE_RootLagWindow_ForwardChainRetention(
uint rootLow, uint forwardLow, float planeX, float eyeZ)
{
var datDir = CornerFloodReplayTests.ResolveDatDir();
@@ -239,7 +242,8 @@ public class Issue176177FacilityHubFloodReplayTests
}
[Fact]
- public void ScenarioD_CorridorWalk_PerStepChurn()
+ [Trait("Purpose", "Diagnostic")]
+ public void Diagnostic_ScenarioD_CorridorWalk_PerStepChurn()
{
var datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; }
diff --git a/tests/AcDream.App.Tests/Rendering/Issue177StairDescentCameraFloodTests.cs b/tests/AcDream.App.Tests/Rendering/Issue177StairDescentCameraFloodTests.cs
index 5cf4e472..f10f0bc7 100644
--- a/tests/AcDream.App.Tests/Rendering/Issue177StairDescentCameraFloodTests.cs
+++ b/tests/AcDream.App.Tests/Rendering/Issue177StairDescentCameraFloodTests.cs
@@ -105,7 +105,8 @@ public class Issue177StairDescentCameraFloodTests
=> string.Join(" ", ids.Select(id => $"{id & 0xFFFFu:X4}"));
[Fact]
- public void StairCellComposition_ShellVsStatics()
+ [Trait("Purpose", "Diagnostic")]
+ public void Diagnostic_StairCellComposition_ShellVsStatics()
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; }
@@ -166,7 +167,8 @@ public class Issue177StairDescentCameraFloodTests
/// locate the knife-edge and which cell flips.
///
[Fact]
- public void RealStaircase_FineYawZoomSweep_FindKnifeEdge()
+ [Trait("Purpose", "Diagnostic")]
+ public void Diagnostic_RealStaircase_FineYawZoomSweep_FindKnifeEdge()
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; }
@@ -228,7 +230,8 @@ public class Issue177StairDescentCameraFloodTests
/// caps well below retail's 26, our flood truncates the spiral (the vanish).
///
[Fact]
- public void FloodDepthFrom015E_VsRetail26()
+ [Trait("Purpose", "Diagnostic")]
+ public void Diagnostic_FloodDepthFrom015E_VsRetail26()
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; }
@@ -349,7 +352,8 @@ public class Issue177StairDescentCameraFloodTests
/// contact leaving the eye on the surface vs free-space boom geometry.
///
[Fact]
- public void StaircaseSweep_EyeClearanceFromCeilingPortal()
+ [Trait("Purpose", "Diagnostic")]
+ public void Diagnostic_StaircaseSweep_EyeClearanceFromCeilingPortal()
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; }
@@ -392,10 +396,11 @@ public class Issue177StairDescentCameraFloodTests
}
[Theory]
+ [Trait("Purpose", "Diagnostic")]
// spot label, player x, player cell, description
[InlineData(90.0f, 0x0178u, "TOP corridor 0178 (approach view)")]
[InlineData(100.0f, 0x0183u, "BOTTOM room 0183 (look-back view)")]
- public void ParkedYawZoomSweep_StairAdmission(float px, uint pcell, string label)
+ public void Diagnostic_ParkedYawZoomSweep_StairAdmission(float px, uint pcell, string label)
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; }
@@ -451,7 +456,8 @@ public class Issue177StairDescentCameraFloodTests
}
[Fact]
- public void Descent_RealCameraSweep_StairCellRetention()
+ [Trait("Purpose", "Diagnostic")]
+ public void Diagnostic_Descent_RealCameraSweep_StairCellRetention()
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; }
diff --git a/tests/AcDream.App.Tests/Rendering/Issue181VisFlapReplayTests.cs b/tests/AcDream.App.Tests/Rendering/Issue181VisFlapReplayTests.cs
index d026eb4d..c1da7914 100644
--- a/tests/AcDream.App.Tests/Rendering/Issue181VisFlapReplayTests.cs
+++ b/tests/AcDream.App.Tests/Rendering/Issue181VisFlapReplayTests.cs
@@ -56,6 +56,7 @@ public class Issue181VisFlapReplayTests
=> string.Join(" ", ids.Select(id => $"{id & 0xFFFFu:X4}"));
[Fact]
+ [Trait("Purpose", "Diagnostic")]
public void Diagnostic_FlappingCellViewRegion_SliverOrLarge()
{
var datDir = CornerFloodReplayTests.ResolveDatDir();
diff --git a/tests/AcDream.App.Tests/Rendering/Issue181WallPressEquilibriumTests.cs b/tests/AcDream.App.Tests/Rendering/Issue181WallPressEquilibriumTests.cs
index 0234a96c..3930723d 100644
--- a/tests/AcDream.App.Tests/Rendering/Issue181WallPressEquilibriumTests.cs
+++ b/tests/AcDream.App.Tests/Rendering/Issue181WallPressEquilibriumTests.cs
@@ -32,6 +32,7 @@ namespace AcDream.App.Tests.Rendering;
/// numbers into hard pins.
///
[Collection(CameraDiagnosticsCollection.Name)]
+[Trait("Purpose", "Diagnostic")]
public class Issue181WallPressEquilibriumTests
{
private const uint FacilityHubLandblock = 0x8A020000u;
diff --git a/tests/AcDream.App.Tests/UI/Layout/FaPanelSlotProbeTests.cs b/tests/AcDream.App.Tests/UI/Layout/FaPanelSlotProbeTests.cs
index 4e6c9b58..afee951e 100644
--- a/tests/AcDream.App.Tests/UI/Layout/FaPanelSlotProbeTests.cs
+++ b/tests/AcDream.App.Tests/UI/Layout/FaPanelSlotProbeTests.cs
@@ -14,6 +14,7 @@ namespace AcDream.App.Tests.UI.Layout;
/// identify the two FA slots definitively: 0x1000026F (fellowship name field)
/// and 0x10000263 (swear-allegiance button). Env-gated like the other probes.
///
+[Trait("Purpose", "Diagnostic")]
public sealed class FaPanelSlotProbeTests
{
[Fact]
diff --git a/tests/AcDream.App.Tests/UI/Layout/MapHousePanelSlotProbeTests.cs b/tests/AcDream.App.Tests/UI/Layout/MapHousePanelSlotProbeTests.cs
index 79cfce5e..e9696d0f 100644
--- a/tests/AcDream.App.Tests/UI/Layout/MapHousePanelSlotProbeTests.cs
+++ b/tests/AcDream.App.Tests/UI/Layout/MapHousePanelSlotProbeTests.cs
@@ -25,6 +25,7 @@ namespace AcDream.App.Tests.UI.Layout;
/// throwaway.
///
///
+[Trait("Purpose", "Diagnostic")]
public sealed class MapHousePanelSlotProbeTests
{
private const uint HostLayoutId = 0x2100006Eu;
diff --git a/tests/AcDream.App.Tests/UI/Layout/OptionsPanelLiveMountProbeTests.cs b/tests/AcDream.App.Tests/UI/Layout/OptionsPanelLiveMountProbeTests.cs
index d5c8e694..540ab27d 100644
--- a/tests/AcDream.App.Tests/UI/Layout/OptionsPanelLiveMountProbeTests.cs
+++ b/tests/AcDream.App.Tests/UI/Layout/OptionsPanelLiveMountProbeTests.cs
@@ -19,6 +19,7 @@ namespace AcDream.App.Tests.UI.Layout;
public sealed class OptionsPanelLiveMountProbeTests
{
[Fact]
+ [Trait("Purpose", "Diagnostic")]
public void ProbeLiveMountShapes()
{
if (Environment.GetEnvironmentVariable("ACDREAM_PROBE_LIVE_MOUNT") != "1")
@@ -314,6 +315,7 @@ public sealed class OptionsPanelLiveMountProbeTests
/// FIXED 6-row + scrollbar popup in retail, user-gated during the vendor
/// campaign) as the contrast control.
[Fact]
+ [Trait("Purpose", "Diagnostic")]
public void ProbeMenuPopupSizingAndTextStyle()
{
if (Environment.GetEnvironmentVariable("ACDREAM_PROBE_LIVE_MOUNT") != "1")
@@ -479,6 +481,7 @@ public sealed class OptionsPanelLiveMountProbeTests
/// to resolve in table 0x23000003 — sweep the plausible tables and key
/// spellings against the live DAT to find their real home.
[Fact]
+ [Trait("Purpose", "Diagnostic")]
public void ProbeFilterLabelHome()
{
if (Environment.GetEnvironmentVariable("ACDREAM_PROBE_LIVE_MOUNT") != "1")
diff --git a/tests/AcDream.App.Tests/UI/Layout/PowerbarLayoutProbeTests.cs b/tests/AcDream.App.Tests/UI/Layout/PowerbarLayoutProbeTests.cs
index 4eb14762..7a79ea84 100644
--- a/tests/AcDream.App.Tests/UI/Layout/PowerbarLayoutProbeTests.cs
+++ b/tests/AcDream.App.Tests/UI/Layout/PowerbarLayoutProbeTests.cs
@@ -18,6 +18,7 @@ namespace AcDream.App.Tests.UI.Layout;
public sealed class PowerbarLayoutProbeTests
{
[Fact]
+ [Trait("Purpose", "Diagnostic")]
public void ProbePowerbarAuthoredStrings()
{
if (Environment.GetEnvironmentVariable("ACDREAM_PROBE_POWERBAR") != "1")
@@ -75,6 +76,7 @@ public sealed class PowerbarLayoutProbeTests
/// gmSecureTradeUI's element tree (lane A gave element ids 0x10000085-8B
/// but not the layout id) and dump it.
[Fact]
+ [Trait("Purpose", "Diagnostic")]
public void ProbeSecureTradeLayout()
{
if (Environment.GetEnvironmentVariable("ACDREAM_PROBE_POWERBAR") != "1")
@@ -107,6 +109,7 @@ public sealed class PowerbarLayoutProbeTests
/// ID_SecureTrade_TotalItemsLabel's fragments + variable hashes so the
/// count text can use ResolveTemplate if token-free (AD-95).
[Fact]
+ [Trait("Purpose", "Diagnostic")]
public void ProbeTotalItemsTemplate()
{
if (Environment.GetEnvironmentVariable("ACDREAM_PROBE_POWERBAR") != "1")
diff --git a/tests/AcDream.App.Tests/UI/SpewBoxLayoutDumpDiagnostic.cs b/tests/AcDream.App.Tests/UI/SpewBoxLayoutDumpDiagnostic.cs
index e7b5f2ef..6d47c7ec 100644
--- a/tests/AcDream.App.Tests/UI/SpewBoxLayoutDumpDiagnostic.cs
+++ b/tests/AcDream.App.Tests/UI/SpewBoxLayoutDumpDiagnostic.cs
@@ -75,6 +75,7 @@ namespace AcDream.App.Tests.UI;
/// States dictionary was not walked) remain open.
///
///
+[Trait("Purpose", "Diagnostic")]
public sealed class SpewBoxLayoutDumpDiagnostic
{
private readonly ITestOutputHelper _out;
diff --git a/tests/AcDream.Core.Tests/Audio/EnvCellSoundEmitterInventoryTests.cs b/tests/AcDream.Core.Tests/Audio/EnvCellSoundEmitterInventoryTests.cs
index 0f6de976..f9f1e24d 100644
--- a/tests/AcDream.Core.Tests/Audio/EnvCellSoundEmitterInventoryTests.cs
+++ b/tests/AcDream.Core.Tests/Audio/EnvCellSoundEmitterInventoryTests.cs
@@ -127,7 +127,8 @@ public sealed class EnvCellSoundEmitterInventoryTests
}
[Fact]
- public void PortalDat_SetupsWithAmbientSlotSoundTables()
+ [Trait("Purpose", "Diagnostic")]
+ public void Diagnostic_PortalDat_SetupsWithAmbientSlotSoundTables()
{
string? datDir = ConformanceDats.ResolveDatDir();
if (datDir is null)
@@ -184,7 +185,8 @@ public sealed class EnvCellSoundEmitterInventoryTests
}
[Fact]
- public void SoundTables_WithAmbientSlots_ExistForWireBinding()
+ [Trait("Purpose", "Diagnostic")]
+ public void Diagnostic_SoundTables_WithAmbientSlots_ExistForWireBinding()
{
string? datDir = ConformanceDats.ResolveDatDir();
if (datDir is null)
diff --git a/tests/AcDream.Core.Tests/Conformance/CottageDoorwayCharacterizationTests.cs b/tests/AcDream.Core.Tests/Conformance/CottageDoorwayCharacterizationTests.cs
index f45d7654..43276346 100644
--- a/tests/AcDream.Core.Tests/Conformance/CottageDoorwayCharacterizationTests.cs
+++ b/tests/AcDream.Core.Tests/Conformance/CottageDoorwayCharacterizationTests.cs
@@ -21,6 +21,7 @@ public class CottageDoorwayCharacterizationTests
public CottageDoorwayCharacterizationTests(ITestOutputHelper output) => _out = output;
[Fact]
+ [Trait("Purpose", "Diagnostic")]
public void Characterize_CottageNeighborhood_PrintStructure()
{
var datDir = ConformanceDats.ResolveDatDir();
@@ -65,6 +66,7 @@ public class CottageDoorwayCharacterizationTests
public const uint OutdoorLandcell0031 = 0xA9B40031u;
[Fact]
+ [Trait("Purpose", "Diagnostic")]
public void Characterize_Doorway_FindInteriorPoints()
{
var datDir = ConformanceDats.ResolveDatDir();
diff --git a/tests/AcDream.Core.Tests/Conformance/DungeonLandblockDatProbeTests.cs b/tests/AcDream.Core.Tests/Conformance/DungeonLandblockDatProbeTests.cs
index e1fe5a96..70d1307c 100644
--- a/tests/AcDream.Core.Tests/Conformance/DungeonLandblockDatProbeTests.cs
+++ b/tests/AcDream.Core.Tests/Conformance/DungeonLandblockDatProbeTests.cs
@@ -16,6 +16,7 @@ namespace AcDream.Core.Tests.Conformance;
/// landblock have a LandBlock (0xXXYYFFFF) terrain record at all, or only
/// LandBlockInfo + EnvCells? Output-only — no assertions.
///
+[Trait("Purpose", "Diagnostic")]
public sealed class DungeonLandblockDatProbeTests
{
private readonly ITestOutputHelper _out;
diff --git a/tests/AcDream.Core.Tests/Conformance/HoltburgTorchFalloffProbeTests.cs b/tests/AcDream.Core.Tests/Conformance/HoltburgTorchFalloffProbeTests.cs
index 1d3a7a41..dfd8ec02 100644
--- a/tests/AcDream.Core.Tests/Conformance/HoltburgTorchFalloffProbeTests.cs
+++ b/tests/AcDream.Core.Tests/Conformance/HoltburgTorchFalloffProbeTests.cs
@@ -20,6 +20,7 @@ namespace AcDream.Core.Tests.Conformance;
/// these falloffs ARE what retail reads (modulo any load-time transform, settled
/// separately in the decomp). Output-only — no assertions; read the log.
///
+[Trait("Purpose", "Diagnostic")]
public sealed class HoltburgTorchFalloffProbeTests
{
private readonly ITestOutputHelper _out;
diff --git a/tests/AcDream.Core.Tests/Conformance/Issue113DoorVanishDiagnosticTests.cs b/tests/AcDream.Core.Tests/Conformance/Issue113DoorVanishDiagnosticTests.cs
index 90f37b7f..010b13c5 100644
--- a/tests/AcDream.Core.Tests/Conformance/Issue113DoorVanishDiagnosticTests.cs
+++ b/tests/AcDream.Core.Tests/Conformance/Issue113DoorVanishDiagnosticTests.cs
@@ -70,6 +70,7 @@ public sealed class Issue113DoorVanishDiagnosticTests
/// vertical door-sized quads.
///
[Fact]
+ [Trait("Purpose", "Diagnostic")]
public void DumpHoltburgBuildings_OrphanGeometry()
{
var datDir = ConformanceDats.ResolveDatDir();
@@ -191,7 +192,8 @@ public sealed class Issue113DoorVanishDiagnosticTests
/// must move.
///
[Fact]
- public void ReplicateProductionEmission_OnPortalFills()
+ [Trait("Purpose", "Diagnostic")]
+ public void Diagnostic_ReplicateProductionEmission_OnPortalFills()
{
var datDir = ConformanceDats.ResolveDatDir();
if (datDir is null) { _out.WriteLine("dats unavailable — skipped"); return; }
@@ -237,6 +239,7 @@ public sealed class Issue113DoorVanishDiagnosticTests
/// which is exactly why retail shows doors but not the ramp.
///
[Fact]
+ [Trait("Purpose", "Diagnostic")]
public void DumpPortalFillSurfaceTypes()
{
var datDir = ConformanceDats.ResolveDatDir();
@@ -282,6 +285,7 @@ public sealed class Issue113DoorVanishDiagnosticTests
/// the phantom geometry, for type-model comparison against the door.
///
[Fact]
+ [Trait("Purpose", "Diagnostic")]
public void DumpControls_HallAndCottage()
{
var datDir = ConformanceDats.ResolveDatDir();
diff --git a/tests/AcDream.Core.Tests/Conformance/Issue113PhantomStairsDumpTests.cs b/tests/AcDream.Core.Tests/Conformance/Issue113PhantomStairsDumpTests.cs
index c0aa6cff..91545d8c 100644
--- a/tests/AcDream.Core.Tests/Conformance/Issue113PhantomStairsDumpTests.cs
+++ b/tests/AcDream.Core.Tests/Conformance/Issue113PhantomStairsDumpTests.cs
@@ -287,6 +287,7 @@ public sealed class Issue113PhantomStairsDumpTests
/// sphere STRADDLES this plane (|dist| < radius + 0.0002, pc:310090-310120).
///
[Fact]
+ [Trait("Purpose", "Diagnostic")]
public void Dump_Cell104_ExteriorPortalPlane_Vs_GapPoint()
{
var datDir = ConformanceDats.ResolveDatDir();
@@ -559,6 +560,7 @@ public sealed class Issue113PhantomStairsDumpTests
/// printed top→bottom DESCENDING (north up).
///
[Fact]
+ [Trait("Purpose", "Diagnostic")]
public void DumpAAB3_Watchtower_TopDownMap()
{
var datDir = ConformanceDats.ResolveDatDir();
@@ -679,6 +681,7 @@ public sealed class Issue113PhantomStairsDumpTests
/// retail skips and our building-mesh extraction mishandles?
///
[Fact]
+ [Trait("Purpose", "Diagnostic")]
public void DumpHallModel_PolyFlagHistogram()
{
var datDir = ConformanceDats.ResolveDatDir();
diff --git a/tests/AcDream.Core.Tests/Conformance/ThresholdDivergenceDiagnosticTests.cs b/tests/AcDream.Core.Tests/Conformance/ThresholdDivergenceDiagnosticTests.cs
index 0f7717db..860b9946 100644
--- a/tests/AcDream.Core.Tests/Conformance/ThresholdDivergenceDiagnosticTests.cs
+++ b/tests/AcDream.Core.Tests/Conformance/ThresholdDivergenceDiagnosticTests.cs
@@ -13,6 +13,7 @@ namespace AcDream.Core.Tests.Conformance;
/// FindCellList result — so the P0→P1 divergence is precisely characterized
/// (real pick-criterion divergence vs a test/sphere artifact).
///
+[Trait("Purpose", "Diagnostic")]
public class ThresholdDivergenceDiagnosticTests
{
private readonly ITestOutputHelper _out;
diff --git a/tests/AcDream.Core.Tests/Physics/CameraCornerSealReplayTests.cs b/tests/AcDream.Core.Tests/Physics/CameraCornerSealReplayTests.cs
index 7977211b..ff5cf518 100644
--- a/tests/AcDream.Core.Tests/Physics/CameraCornerSealReplayTests.cs
+++ b/tests/AcDream.Core.Tests/Physics/CameraCornerSealReplayTests.cs
@@ -129,6 +129,7 @@ public class CameraCornerSealReplayTests
/// shadow-query path is the half that failed).
///
[Fact]
+ [Trait("Purpose", "Diagnostic")]
public void Diagnostic_DispatchTrace_LeakPath_vs_Controls()
{
var datDir = ConformanceDats.ResolveDatDir();
diff --git a/tests/AcDream.Core.Tests/Physics/DoorSetupGfxObjInspectionTests.cs b/tests/AcDream.Core.Tests/Physics/DoorSetupGfxObjInspectionTests.cs
index 4bd1bac0..f3e72b9a 100644
--- a/tests/AcDream.Core.Tests/Physics/DoorSetupGfxObjInspectionTests.cs
+++ b/tests/AcDream.Core.Tests/Physics/DoorSetupGfxObjInspectionTests.cs
@@ -237,6 +237,7 @@ public class DoorSetupGfxObjInspectionTests
// computes a different outdoor cell than 0xA9B40029
// Findings drive the fix direction.
[Fact]
+ [Trait("Purpose", "Diagnostic")]
public void HoltburgCottage_CellPortals_DatInspection()
{
var datDir = Env.GetEnvironmentVariable("ACDREAM_DAT_DIR")
@@ -271,6 +272,7 @@ public class DoorSetupGfxObjInspectionTests
/// building wall sub-piece) lives at that XY.
///
[Fact]
+ [Trait("Purpose", "Diagnostic")]
public void HoltburgLandblockStatics_DatInspection()
{
var datDir = Env.GetEnvironmentVariable("ACDREAM_DAT_DIR")
diff --git a/tests/AcDream.Core.Tests/Physics/Issue137CorridorSeamInspectionTests.cs b/tests/AcDream.Core.Tests/Physics/Issue137CorridorSeamInspectionTests.cs
index f901d4ca..6a63d73c 100644
--- a/tests/AcDream.Core.Tests/Physics/Issue137CorridorSeamInspectionTests.cs
+++ b/tests/AcDream.Core.Tests/Physics/Issue137CorridorSeamInspectionTests.cs
@@ -280,6 +280,7 @@ public class Issue137CorridorSeamInspectionTests
/// hit point + normal.
///
[Fact]
+ [Trait("Purpose", "Diagnostic")]
public void CorridorSeam_FindPolygonMatchingLiveHit()
{
var datDir = Env.GetEnvironmentVariable("ACDREAM_DAT_DIR")
diff --git a/tests/AcDream.Core.Tests/Physics/Issue186ConnectorCellGeometryInspectionTests.cs b/tests/AcDream.Core.Tests/Physics/Issue186ConnectorCellGeometryInspectionTests.cs
index 57e86e70..dcf8d68f 100644
--- a/tests/AcDream.Core.Tests/Physics/Issue186ConnectorCellGeometryInspectionTests.cs
+++ b/tests/AcDream.Core.Tests/Physics/Issue186ConnectorCellGeometryInspectionTests.cs
@@ -33,6 +33,7 @@ public class Issue186ConnectorCellGeometryInspectionTests
}
[Fact]
+ [Trait("Purpose", "Diagnostic")]
public void Dump_ConnectorCells_ShellAndCollision()
{
var datDir = DatDir();
diff --git a/tests/AcDream.Core.Tests/Physics/Issue188FadingDoorMotionTableInspectionTests.cs b/tests/AcDream.Core.Tests/Physics/Issue188FadingDoorMotionTableInspectionTests.cs
index 289c97c4..a78498d1 100644
--- a/tests/AcDream.Core.Tests/Physics/Issue188FadingDoorMotionTableInspectionTests.cs
+++ b/tests/AcDream.Core.Tests/Physics/Issue188FadingDoorMotionTableInspectionTests.cs
@@ -25,6 +25,7 @@ namespace AcDream.Core.Tests.Physics;
/// wired). Kept as a reusable decoder for any future "why doesn't this
/// animate" question — swap the MotionTableId.
///
+[Trait("Purpose", "Diagnostic")]
public class Issue188FadingDoorMotionTableInspectionTests
{
private readonly ITestOutputHelper _out;
diff --git a/tests/AcDream.Core.Tests/Rendering/Issue176177DungeonSeamInspectionTests.cs b/tests/AcDream.Core.Tests/Rendering/Issue176177DungeonSeamInspectionTests.cs
index 5013fb1d..54064199 100644
--- a/tests/AcDream.Core.Tests/Rendering/Issue176177DungeonSeamInspectionTests.cs
+++ b/tests/AcDream.Core.Tests/Rendering/Issue176177DungeonSeamInspectionTests.cs
@@ -244,7 +244,8 @@ public class Issue176177DungeonSeamInspectionTests
/// light exposed it.
///
[Fact]
- public void CorridorNeighborhood_CoplanarOverlappingDrawnPolyPairs()
+ [Trait("Purpose", "Diagnostic")]
+ public void Diagnostic_CorridorNeighborhood_CoplanarOverlappingDrawnPolyPairs()
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; }
@@ -470,7 +471,8 @@ public class Issue176177DungeonSeamInspectionTests
[InlineData(0x8A02016Eu)] // corridor cells — the striped-floor screenshot area
[InlineData(0x8A02017Au)]
[InlineData(0x8A020165u)]
- public void UnderHall_DrawnPolys_SurfaceColors(uint cellId)
+ [Trait("Purpose", "Diagnostic")]
+ public void Diagnostic_UnderHall_DrawnPolys_SurfaceColors(uint cellId)
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; }
@@ -635,6 +637,7 @@ public class Issue176177DungeonSeamInspectionTests
[Theory]
[InlineData(0x8A02015Eu)] // #176 repro corridor
[InlineData(0x8A02016Eu)] // corridor with floor-portals
+ [Trait("Purpose", "Diagnostic")]
public void CellVertexNormals_SmoothOrFaceted_Dump(uint cellId)
{
var datDir = ResolveDatDir();
diff --git a/tests/AcDream.Core.Tests/Rendering/Issue93TownNetworkFountainRoomLightInspectionTests.cs b/tests/AcDream.Core.Tests/Rendering/Issue93TownNetworkFountainRoomLightInspectionTests.cs
index 9b6a228b..cc820caf 100644
--- a/tests/AcDream.Core.Tests/Rendering/Issue93TownNetworkFountainRoomLightInspectionTests.cs
+++ b/tests/AcDream.Core.Tests/Rendering/Issue93TownNetworkFountainRoomLightInspectionTests.cs
@@ -50,6 +50,7 @@ public class Issue93TownNetworkFountainRoomLightInspectionTests
[InlineData(0x00070146u)]
[InlineData(0x00070155u)]
[InlineData(0x00070157u)]
+ [Trait("Purpose", "Diagnostic")]
public void StaticObjects_SetupLightsCount_Dump(uint cellId)
{
var datDir = ResolveDatDir();
diff --git a/tools/audit-test-inventory.ps1 b/tools/audit-test-inventory.ps1
new file mode 100644
index 00000000..5eb93737
--- /dev/null
+++ b/tools/audit-test-inventory.ps1
@@ -0,0 +1,374 @@
+<#
+.SYNOPSIS
+ Produces a syntax-based inventory of every attributed C# test method.
+
+.DESCRIPTION
+ Parses tracked C# files under tests/ with the Roslyn assemblies bundled in
+ the active .NET SDK. The report is evidence for the R3 test-truth audit: it
+ records test attributes and traits, static skips, empty-return sites,
+ assertion/throw signals (including same-file helper calls), diagnostic
+ output signals, wall-clock waits, environment-variable dependencies, and
+ source-text reads.
+
+ This is a candidate generator, not a semantic proof. In particular, an
+ output-only candidate can call a failure-capable helper declared in another
+ file or base class. Every removal or lane change still requires inspection.
+#>
+[CmdletBinding()]
+param(
+ [string]$OutputPath = 'artifacts/test-audit/test-inventory.json'
+)
+
+Set-StrictMode -Version Latest
+$ErrorActionPreference = 'Stop'
+
+$repoRoot = [IO.Path]::GetFullPath((Split-Path -Parent $PSScriptRoot))
+$resolvedOutput = if ([IO.Path]::IsPathRooted($OutputPath)) {
+ [IO.Path]::GetFullPath($OutputPath)
+} else {
+ [IO.Path]::GetFullPath((Join-Path $repoRoot $OutputPath))
+}
+
+$dotnetCommand = Get-Command dotnet -ErrorAction Stop
+$dotnetRoot = Split-Path -Parent $dotnetCommand.Source
+$sdkVersion = (& dotnet --version).Trim()
+$roslynDirectory = Join-Path $dotnetRoot "sdk/$sdkVersion/Roslyn/bincore"
+$loadedRoslyn = @([AppDomain]::CurrentDomain.GetAssemblies() | Where-Object {
+ $_.GetName().Name -in @('Microsoft.CodeAnalysis', 'Microsoft.CodeAnalysis.CSharp')
+ })
+if ($loadedRoslyn.Count -lt 2) {
+ foreach ($assemblyName in 'Microsoft.CodeAnalysis.dll', 'Microsoft.CodeAnalysis.CSharp.dll') {
+ $assemblyPath = Join-Path $roslynDirectory $assemblyName
+ if (-not (Test-Path -LiteralPath $assemblyPath -PathType Leaf)) {
+ throw "Roslyn assembly not found at '$assemblyPath'."
+ }
+ Add-Type -Path $assemblyPath
+ }
+}
+$roslynVersion = [Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree].Assembly.GetName().Version.ToString()
+
+function Get-ShortAttributeName {
+ param([Parameter(Mandatory)]$Attribute)
+
+ $name = $Attribute.Name.ToString().Split('.')[-1]
+ if ($name.EndsWith('Attribute', [StringComparison]::Ordinal)) {
+ $name = $name.Substring(0, $name.Length - 'Attribute'.Length)
+ }
+ return $name
+}
+
+function Get-NodeLine {
+ param(
+ [Parameter(Mandatory)]$Tree,
+ [Parameter(Mandatory)]$Node
+ )
+
+ return $Tree.GetLineSpan($Node.Span).StartLinePosition.Line + 1
+}
+
+function Get-ContainingClassName {
+ param([Parameter(Mandatory)]$Method)
+
+ $typeNode = Get-ContainingTypeNode $Method
+ if ($null -ne $typeNode) {
+ return $typeNode.Identifier.Text
+ }
+ return ''
+}
+
+function Get-ContainingTypeNode {
+ param([Parameter(Mandatory)]$Method)
+
+ $cursor = $Method.Parent
+ while ($null -ne $cursor) {
+ if ($cursor.GetType().Name -in @(
+ 'ClassDeclarationSyntax',
+ 'RecordDeclarationSyntax',
+ 'StructDeclarationSyntax')) {
+ return $cursor
+ }
+ $cursor = $cursor.Parent
+ }
+ return $null
+}
+
+function Get-ReturnGateKind {
+ param([Parameter(Mandatory)][string]$Condition)
+
+ if ($Condition -match '(?i)(GetEnvironmentVariable|ACDREAM_)') {
+ return 'OptIn'
+ }
+ if ($Condition -match '(?i)(OperatingSystem\.)') {
+ return 'Platform'
+ }
+ if ($Condition -match '(?i)(datDir|datDirectory|Directory\.Exists|File\.Exists|TryOpen|TryBuildScenario|steps is null|csvPath|\.pak)') {
+ return 'ExternalAsset'
+ }
+ return $null
+}
+
+function Get-InvocationName {
+ param([Parameter(Mandatory)]$Invocation)
+
+ $expression = $Invocation.Expression
+ if ($expression.GetType().Name -eq 'MemberAccessExpressionSyntax') {
+ return $expression.Name.Identifier.Text
+ }
+ if ($expression.GetType().Name -eq 'IdentifierNameSyntax') {
+ return $expression.Identifier.Text
+ }
+ if ($expression.GetType().Name -eq 'GenericNameSyntax') {
+ return $expression.Identifier.Text
+ }
+ return $expression.ToString()
+}
+
+function Test-DirectFailureSignal {
+ param([Parameter(Mandatory)]$Method)
+
+ if (@($Method.DescendantNodes() | Where-Object {
+ $_.GetType().Name -in @('ThrowStatementSyntax', 'ThrowExpressionSyntax')
+ }).Count -gt 0) {
+ return $true
+ }
+
+ foreach ($invocation in @($Method.DescendantNodes() | Where-Object {
+ $_.GetType().Name -eq 'InvocationExpressionSyntax'
+ })) {
+ $expression = $invocation.Expression.ToString()
+ if ($expression -match '(^|\.)Assert(\.|$)' -or
+ $expression -match '(^|\.)(Should|Shouldly)(\.|$)' -or
+ $expression -match '(^|\.)Verify($|\.)') {
+ return $true
+ }
+ }
+ return $false
+}
+
+function Test-DirectOutputSignal {
+ param([Parameter(Mandatory)]$Method)
+
+ foreach ($invocation in @($Method.DescendantNodes() | Where-Object {
+ $_.GetType().Name -eq 'InvocationExpressionSyntax'
+ })) {
+ $expression = $invocation.Expression.ToString()
+ if ($expression -match '(^|\.)(_?out|output|Console|Debug|Trace)\.(Write|WriteLine)$' -or
+ $expression -match '(^|\.)(WriteDiagnostic|Dump|DumpSteps|Print)(\.|$)') {
+ return $true
+ }
+ }
+ return $false
+}
+
+function Test-RecursiveSignal {
+ param(
+ [Parameter(Mandatory)][string]$MethodKey,
+ [Parameter(Mandatory)][hashtable]$MethodMap,
+ [Parameter(Mandatory)][hashtable]$DirectSignalMap,
+ [Parameter(Mandatory)][hashtable]$CallMap,
+ [Parameter(Mandatory)][hashtable]$Visited
+ )
+
+ if ($Visited.ContainsKey($MethodKey)) {
+ return $false
+ }
+ $Visited[$MethodKey] = $true
+ if ($DirectSignalMap[$MethodKey]) {
+ return $true
+ }
+ foreach ($calledName in @($CallMap[$MethodKey])) {
+ foreach ($candidateKey in @($MethodMap.Keys | Where-Object {
+ $_.EndsWith("::$calledName", [StringComparison]::Ordinal)
+ })) {
+ if (Test-RecursiveSignal $candidateKey $MethodMap $DirectSignalMap $CallMap $Visited) {
+ return $true
+ }
+ }
+ }
+ return $false
+}
+
+$trackedFiles = @(& git -C $repoRoot ls-files tests | Where-Object {
+ $_.EndsWith('.cs', [StringComparison]::OrdinalIgnoreCase)
+ })
+if ($LASTEXITCODE -ne 0) {
+ throw 'git ls-files failed while enumerating test sources.'
+}
+
+$records = [Collections.Generic.List[object]]::new()
+$parsedFileCount = 0
+foreach ($relativePath in $trackedFiles) {
+ $absolutePath = Join-Path $repoRoot $relativePath
+ if (-not (Test-Path -LiteralPath $absolutePath -PathType Leaf)) {
+ continue
+ }
+
+ $source = [IO.File]::ReadAllText($absolutePath)
+ $tree = [Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree]::ParseText($source)
+ $root = $tree.GetRoot()
+ $parsedFileCount++
+
+ $allMethods = @($root.DescendantNodes() | Where-Object {
+ $_.GetType().Name -eq 'MethodDeclarationSyntax'
+ })
+ $methodMap = @{}
+ $failureMap = @{}
+ $outputMap = @{}
+ $callMap = @{}
+
+ foreach ($method in $allMethods) {
+ $className = Get-ContainingClassName $method
+ $key = "$className::$($method.Identifier.Text)"
+ $methodMap[$key] = $method
+ $failureMap[$key] = Test-DirectFailureSignal $method
+ $outputMap[$key] = Test-DirectOutputSignal $method
+ $callMap[$key] = @($method.DescendantNodes() |
+ Where-Object { $_.GetType().Name -eq 'InvocationExpressionSyntax' } |
+ ForEach-Object { Get-InvocationName $_ } |
+ Sort-Object -Unique)
+ }
+
+ foreach ($method in $allMethods) {
+ $methodAttributes = @($method.AttributeLists | ForEach-Object { $_.Attributes })
+ $attributeNames = @($methodAttributes | ForEach-Object {
+ Get-ShortAttributeName $_
+ })
+ $testAttributes = @($attributeNames | Where-Object {
+ $_ -in @('Fact', 'Theory', 'AvaloniaFact', 'InstalledDatFact')
+ })
+ if ($testAttributes.Count -eq 0) {
+ continue
+ }
+
+ $className = Get-ContainingClassName $method
+ $key = "$className::$($method.Identifier.Text)"
+ $hasFailureSignal = Test-RecursiveSignal `
+ $key $methodMap $failureMap $callMap @{}
+ $hasOutputSignal = Test-RecursiveSignal `
+ $key $methodMap $outputMap $callMap @{}
+
+ $emptyReturns = [Collections.Generic.List[object]]::new()
+ foreach ($returnNode in @($method.DescendantNodes() | Where-Object {
+ $_.GetType().Name -eq 'ReturnStatementSyntax' -and
+ $null -eq $_.Expression
+ })) {
+ $condition = ''
+ $cursor = $returnNode.Parent
+ while ($null -ne $cursor -and $cursor -ne $method) {
+ if ($cursor.GetType().Name -eq 'IfStatementSyntax') {
+ $condition = $cursor.Condition.ToString()
+ break
+ }
+ $cursor = $cursor.Parent
+ }
+ $emptyReturns.Add([ordered]@{
+ Line = Get-NodeLine $tree $returnNode
+ Condition = $condition
+ GateKind = Get-ReturnGateKind $condition
+ })
+ }
+
+ $traits = [Collections.Generic.List[string]]::new()
+ $staticSkip = $null
+ $typeNode = Get-ContainingTypeNode $method
+ $traitAttributes = @($methodAttributes)
+ if ($null -ne $typeNode) {
+ $traitAttributes += @($typeNode.AttributeLists | ForEach-Object {
+ $_.Attributes
+ })
+ }
+ foreach ($attribute in $traitAttributes) {
+ $attributeName = Get-ShortAttributeName $attribute
+ $arguments = if ($null -eq $attribute.ArgumentList) {
+ ''
+ } else {
+ $attribute.ArgumentList.ToString()
+ }
+ if ($attributeName -eq 'Trait') {
+ $traits.Add($arguments.Trim('(', ')').Replace('"', ''))
+ }
+ }
+ foreach ($attribute in $methodAttributes) {
+ $arguments = if ($null -eq $attribute.ArgumentList) {
+ ''
+ } else {
+ $attribute.ArgumentList.ToString()
+ }
+ if ($arguments -match '(^|[,\s])Skip\s*=') {
+ $staticSkip = $arguments
+ }
+ }
+
+ $bodyText = $method.ToString()
+ $environmentVariables = @([regex]::Matches(
+ $bodyText,
+ 'GetEnvironmentVariable\s*\(\s*"([A-Za-z0-9_]+)"') |
+ ForEach-Object { $_.Groups[1].Value } |
+ Sort-Object -Unique)
+
+ $records.Add([ordered]@{
+ Path = $relativePath.Replace('\', '/')
+ Line = Get-NodeLine $tree $method
+ Class = $className
+ Method = $method.Identifier.Text
+ Attributes = $testAttributes
+ Traits = @($traits)
+ StaticSkip = $staticSkip
+ EmptyReturns = @($emptyReturns)
+ PrerequisiteReturnCandidate = @($emptyReturns | Where-Object {
+ $null -ne $_.GateKind
+ }).Count -gt 0
+ HasFailureSignal = $hasFailureSignal
+ HasOutputSignal = $hasOutputSignal
+ OutputOnlyCandidate = $hasOutputSignal -and -not $hasFailureSignal
+ EnvironmentVariables = $environmentVariables
+ HasThreadSleep = $bodyText -match '\bThread\.Sleep\s*\('
+ HasTaskDelay = $bodyText -match '\bTask\.Delay\s*\('
+ ReadsSourceText = $bodyText -match '(ReadAllText|ReadAllLines)\s*\(' -and
+ $bodyText -match '\.cs'
+ })
+ }
+}
+
+$orderedRecords = @($records | Sort-Object Path, Line, Method)
+$workingTreeStatus = @(& git -C $repoRoot status --short --untracked-files=no)
+$summary = [ordered]@{
+ GeneratedUtc = [DateTime]::UtcNow.ToString('O')
+ RepositoryCommit = (& git -C $repoRoot rev-parse HEAD).Trim()
+ WorkingTreeDirty = $workingTreeStatus.Count -gt 0
+ DotnetSdkVersion = $sdkVersion
+ RoslynAssemblyVersion = $roslynVersion
+ TrackedCSharpFiles = $trackedFiles.Count
+ ParsedCSharpFiles = $parsedFileCount
+ AttributedTestMethods = $orderedRecords.Count
+ StaticSkipMethods = @($orderedRecords | Where-Object { $null -ne $_.StaticSkip }).Count
+ EmptyReturnMethods = @($orderedRecords | Where-Object { $_.EmptyReturns.Count -gt 0 }).Count
+ EmptyReturnSites = @($orderedRecords | ForEach-Object { $_.EmptyReturns }).Count
+ PrerequisiteReturnCandidates = @($orderedRecords | Where-Object {
+ $_.PrerequisiteReturnCandidate
+ }).Count
+ OutputOnlyCandidates = @($orderedRecords | Where-Object { $_.OutputOnlyCandidate }).Count
+ DiagnosticMethods = @($orderedRecords | Where-Object {
+ $_.Traits -contains 'Purpose, Diagnostic'
+ }).Count
+ KnownFailureMethods = @($orderedRecords | Where-Object {
+ $_.Traits -contains 'Status, KnownFailure'
+ }).Count
+ ThreadSleepMethods = @($orderedRecords | Where-Object { $_.HasThreadSleep }).Count
+ TaskDelayMethods = @($orderedRecords | Where-Object { $_.HasTaskDelay }).Count
+ DirectEnvironmentVariableMethods = @($orderedRecords | Where-Object {
+ $_.EnvironmentVariables.Count -gt 0
+ }).Count
+ SourceTextReadMethods = @($orderedRecords | Where-Object { $_.ReadsSourceText }).Count
+}
+
+$report = [ordered]@{
+ Summary = $summary
+ Tests = $orderedRecords
+}
+$outputDirectory = Split-Path -Parent $resolvedOutput
+[IO.Directory]::CreateDirectory($outputDirectory) | Out-Null
+$report | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $resolvedOutput -Encoding utf8
+
+$summary | Format-List
+Write-Output "Inventory: $resolvedOutput"