Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

### Added

- **Architecture-fitness suite — `tests/Fallout.Architecture.Tests`** (#95). A solution-wide [ArchUnitNET](https://github.com/TNG/ArchUnitNET) suite that locks in the runtime layering so future work can't silently invert a dependency edge that still compiles. Rules are scoped by **assembly** (the legacy NUKE namespaces span assemblies, so namespace-based layering would be meaningless): foundation purity (`Fallout.Core` / `Fallout.Utilities` depend on nothing else in-repo), utility-satellite confinement, downward-only layering for `Tooling`/`ProjectModel`/`Build.Shared`/`Solution`, "nothing depends on the `Fallout.Cli` composition root", and "`Fallout.*` never depends on the `Nuke.*` shims". Known-broken areas are governed by a **one-way ratchet**: each rule is allowed exactly its documented baseline of pre-existing violations (`KnownViolations`), and the test fails on any *new* violation or any baseline entry that has been fixed — so the debt can only shrink. The headline debt rule (a type's namespace should be rooted at its assembly name) ships with a ~220-entry baseline capturing today's `Fallout.Common.*` namespace sprawl, which the onion-architecture refactor will whittle down. Migrated the issue-#88 `Fallout.Core` purity guard off the unmaintained `NetArchTest.Rules` (last release 2023) onto ArchUnitNET and retired that package. See [docs/architecture-tests.md](docs/architecture-tests.md).
- **Reintroduced `.editorconfig` and `fallout.slnx.DotSettings`** to restore shared IDE configuration for Rider and Visual Studio. Corresponding Rule 6 in `AGENTS.md` removed.
- **Extracted `Fallout.Core` — the pure domain + graph foundation** (#88, v11 plugin-architecture foundation). New bottom-layer project (`netstandard2.1;net10.0`) holding the side-effect-free types of the build execution pipeline: `ITargetModel` (read-only target projection — the seed for the v12 plugin SDK), `ExecutionStatus`, and `TopoSort` / `PlanResult<T>` (pure topological sort + strongly-connected-component cycle detection). `Fallout.Core` references nothing else in the repo and is held to a strict purity bar — no `System.IO`, `Process`, `Console`, or Serilog — enforced by a `NetArchTest` architecture-fitness test (broader suite tracked in #95).
- **Extracted `Fallout.Core` — the pure domain + graph foundation** (#88, v11 plugin-architecture foundation). New bottom-layer project (`netstandard2.1;net10.0`) holding the side-effect-free types of the build execution pipeline: `ITargetModel` (read-only target projection — the seed for the v12 plugin SDK), `ExecutionStatus`, and `TopoSort` / `PlanResult<T>` (pure topological sort + strongly-connected-component cycle detection). `Fallout.Core` references nothing else in the repo and is held to a strict purity bar — no `System.IO`, `Process`, `Console`, or Serilog — enforced by an `ArchUnitNET` architecture-fitness test (the broader suite, #95, now lives in `tests/Fallout.Architecture.Tests`).
- **Fully backward-compatible.** `ExecutionStatus` moved assemblies (`Fallout.Build` → `Fallout.Core`) but is `[TypeForwardedTo]`, and namespaces are unchanged, so existing consumer `using Fallout.Common.Execution;` code compiles untouched. The live `ExecutableTarget` stays in `Fallout.Build` and now implements `ITargetModel`; `ExecutionPlanner` is reduced to a thin orchestration wrapper over `Fallout.Core`'s pure `TopoSort` (it still owns reading the `strict` parameter, failing the build on a cycle, and mutating target state).
- The graph primitives (`Vertex` / `StronglyConnectedComponent*`) moved from `Fallout.Utilities` into `Fallout.Core` as an internal implementation detail of `TopoSort`; they had no external consumers.

Expand Down
3 changes: 2 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@
<PackageVersion Include="FluentAssertions" Version="8.10.0" />
<PackageVersion Include="GitHubActionsTestLogger" Version="2.4.1" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageVersion Include="NetArchTest.Rules" Version="1.3.2" />
<PackageVersion Include="TngTech.ArchUnitNET" Version="0.13.3" />
<PackageVersion Include="TngTech.ArchUnitNET.xUnit" Version="0.13.3" />
<!-- <PackageVersion Include="TeamCity.VSTest.TestAdapter" Version="1.0.41" />-->
<PackageVersion Include="Verify.Xunit" Version="31.7.1" />
<PackageVersion Include="Verify.DiffPlex" Version="3.1.2" />
Expand Down
73 changes: 73 additions & 0 deletions docs/architecture-tests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Architecture-fitness tests

`tests/Fallout.Architecture.Tests` is a solution-wide [ArchUnitNET](https://github.com/TNG/ArchUnitNET) suite
that asserts Fallout's intended shape so it can't drift as new code lands. It's an ordinary xUnit project — the
rules run inside `[Fact]`/`[Theory]` and fail the build like any other test (no separate tool to run).

It tracks issue [#95](https://github.com/ChrisonSimtian/Fallout/issues/95). The earlier `Fallout.Core` purity
guard from #88 was migrated here off the (unmaintained) `NetArchTest.Rules`.

## Why scoping is by *assembly*, not namespace

Layering in this repo is a project/assembly concern. The legacy NUKE namespaces deliberately span several
assemblies — `Fallout.Common.*` types live in `Fallout.Build`, `Fallout.Build.Shared` and `Fallout.Common`;
the `Fallout.Utilities.Net` assembly still emits `Fallout.Common.Utilities.Net.*` — so a namespace-based layering
rule would be meaningless. Every rule scopes its subject and target with `ResideInAssembly(...)`.

The shared helper `FalloutArchitecture.TypesIn(...)` also constrains every subject/target to first-party
(`Fallout.*` / `Nuke.*`) namespaces. That strips the no-namespace, tooling-generated types injected into every
assembly (`ThisAssembly` from Nerdbank.GitVersioning, `RefSafetyRulesAttribute`, coverlet instrumentation), which
would otherwise read as spurious cross-assembly dependencies.

## What's governed

The reference set in `Fallout.Architecture.Tests.csproj` **is the contract** — an assembly is scanned only if it's
referenced (so it lands in the test output and gets loaded). Adding a production assembly without adding it there
silently drops it from the gate. Deliberately out of scope: the Roslyn build-time tooling
(`Fallout.SourceGenerators`, `Fallout.Tooling.Generator`, `Fallout.Migrate[.Analyzers]`, `Fallout.MSBuildTasks`)
and the vendored `Fallout.Persistence.Solution` parser.

Current rules:

| File | Rule(s) |
|---|---|
| `LayeringTests` | `Core` / `Utilities` are foundations (no in-repo deps); utility satellites depend only on `Utilities`; `Tooling` / `ProjectModel` / `Build.Shared` don't reach upward; `Solution` is a thin facade; nothing depends on the `Cli` composition root; `Fallout.*` never depends on the `Nuke.*` shims. |
| `PurityTests` | `Fallout.Core` takes no dependency on `System.IO`, `System.Diagnostics.Process`, `System.Console`, or Serilog (issue #88). |
| `NamingTests` | A type's namespace should be rooted at its assembly name. The main debt-bearing rule. |

## The ratchet

The architecture is known to be partially broken, so rules aren't all strict. Each rule is enforced by
`Ratchet.Enforce(rule, because, baseline)` against a baseline of known violations in `KnownViolations`:

- a violation **not** in the baseline fails the test — a new regression;
- a baseline entry that **no longer** violates also fails the test — the architecture improved, so the stale
entry must be deleted to lock the gain in.

The baseline can therefore only shrink. Invariants that already hold pass `KnownViolations.None` (zero tolerance).
The naming rule ships with a ~220-entry baseline (`NamespaceAssemblyDrift`) capturing today's `Fallout.Common.*`
sprawl; it shrinks as the onion-architecture refactor (ADR-0006, on `refactor/architecture`) renames things.

## Working with it

**A rule went red on your change.** Don't reflexively add the offender to `KnownViolations`. First decide:
is the new dependency *wrong* (fix the code — the rule just did its job) or *intended* (add it to the relevant
list with a one-line justification)? Baseline keys are type full names, exactly as ArchUnitNET reports them — including nested types
(`Fallout.Common.CI.Partition+TypeConverter`) and generic arity (`RequiresAttribute` + backtick + `1`).

**You fixed some debt.** The test will now fail telling you which baseline entries are stale — delete them.

**Regenerating the drift baseline wholesale** (e.g. after a large rename):

```powershell
dotnet test tests/Fallout.Architecture.Tests/Fallout.Architecture.Tests.csproj
```

The failure message for `Types_reside_in_a_namespace_rooted_at_their_assembly_name` lists every current offender
(the `Offenders:` block). Replace `KnownViolations.NamespaceAssemblyDrift` with that sorted, de-duplicated set.

## Adding a rule

Write the ArchUnitNET rule with `FalloutArchitecture.TypesIn(...)` for the subject/target and pass it through
`Ratchet.Enforce` with a `because` string and a baseline (`KnownViolations.None` if it should be strict). Use the
assembly-name constants on `FalloutArchitecture` rather than string literals.
2 changes: 1 addition & 1 deletion docs/dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ Still on Matt's personal NuGet account; supply-chain SPOF, high-priority to repl
| `coverlet.msbuild` | Code coverage |
| `GitHubActionsTestLogger` | Format test output for GitHub Actions annotations |
| `Basic.Reference.Assemblies.NetStandard20` | Reference assemblies for source-generator compile tests |
| `NetArchTest.Rules` | Architecture-fitness tests (e.g. `Fallout.Core` purity); broader suite tracked in #95 |
| `TngTech.ArchUnitNET` (+ `.xUnit`) | Architecture-fitness suite (`tests/Fallout.Architecture.Tests`, #95) — assembly layering, `Fallout.Core` purity, shim direction, and namespace/assembly alignment, enforced as a ratcheting baseline. Replaced the unmaintained `NetArchTest.Rules`. |

## Build-time CLI tools (`PackageDownload`)

Expand Down
1 change: 1 addition & 0 deletions fallout.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
<Project Path="src\Fallout.Utilities.Text.Json\Fallout.Utilities.Text.Json.csproj" />
<Project Path="src\Fallout.Utilities.Text.Yaml\Fallout.Utilities.Text.Yaml.csproj" />
<Project Path="src\Fallout.Utilities\Fallout.Utilities.csproj" />
<Project Path="tests\Fallout.Architecture.Tests\Fallout.Architecture.Tests.csproj" />
<Project Path="tests\Fallout.Build.Tests\Fallout.Build.Tests.csproj" />
<Project Path="tests\Fallout.Common.Tests\Fallout.Common.Tests.csproj" />
<Project Path="tests\Fallout.Components.Tests\Fallout.Components.Tests.csproj" />
Expand Down
65 changes: 65 additions & 0 deletions tests/Fallout.Architecture.Tests/Fallout.Architecture.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="TngTech.ArchUnitNET" />
<PackageReference Include="TngTech.ArchUnitNET.xUnit" />
</ItemGroup>

<!--
The reference set IS the contract. The fitness rules govern only the assemblies that are
loaded at runtime, and an assembly is loaded only if it lands in this project's output — i.e.
only if it is referenced here. Adding a production assembly to the repo without adding it here
silently drops it from the architecture gate, so this list must track src/ for the runtime
libraries below.

Deliberately OUT of scope (not referenced): the Roslyn build-time tooling
(Fallout.SourceGenerators, Fallout.Tooling.Generator, Fallout.Migrate[.Analyzers],
Fallout.MSBuildTasks) and the vendored Fallout.Persistence.Solution parser. They are
composition-root / build-time / vendored code outside the runtime layering, with rules of their
own (or none). Fallout.Persistence.Solution still arrives transitively via Fallout.Solution and
can appear as a forbidden dependency *target*, but is never a governed *subject*.
-->

<!-- Foundation: depend on nothing else in the repo. -->
<ItemGroup>
<ProjectReference Include="..\..\src\Fallout.Core\Fallout.Core.csproj" />
<ProjectReference Include="..\..\src\Fallout.Utilities\Fallout.Utilities.csproj" />
</ItemGroup>

<!-- Utility satellites: may depend only on Fallout.Utilities. -->
<ItemGroup>
<ProjectReference Include="..\..\src\Fallout.Utilities.IO.Compression\Fallout.Utilities.IO.Compression.csproj" />
<ProjectReference Include="..\..\src\Fallout.Utilities.IO.Globbing\Fallout.Utilities.IO.Globbing.csproj" />
<ProjectReference Include="..\..\src\Fallout.Utilities.Net\Fallout.Utilities.Net.csproj" />
<ProjectReference Include="..\..\src\Fallout.Utilities.Text.Json\Fallout.Utilities.Text.Json.csproj" />
<ProjectReference Include="..\..\src\Fallout.Utilities.Text.Yaml\Fallout.Utilities.Text.Yaml.csproj" />
</ItemGroup>

<!-- Mid layers. -->
<ItemGroup>
<ProjectReference Include="..\..\src\Persistence\Fallout.Solution\Fallout.Solution.csproj" />
<ProjectReference Include="..\..\src\Fallout.Tooling\Fallout.Tooling.csproj" />
<ProjectReference Include="..\..\src\Fallout.ProjectModel\Fallout.ProjectModel.csproj" />
<ProjectReference Include="..\..\src\Fallout.Build.Shared\Fallout.Build.Shared.csproj" />
</ItemGroup>

<!-- Upper layers + composition root. -->
<ItemGroup>
<ProjectReference Include="..\..\src\Fallout.Build\Fallout.Build.csproj" />
<ProjectReference Include="..\..\src\Fallout.Common\Fallout.Common.csproj" />
<ProjectReference Include="..\..\src\Fallout.Components\Fallout.Components.csproj" />
<ProjectReference Include="..\..\src\Fallout.Cli\Fallout.Cli.csproj" />
</ItemGroup>

<!-- Transition shims: must point inward (Nuke.* -> Fallout.*), never the reverse. -->
<ItemGroup>
<ProjectReference Include="..\..\src\Shims\Nuke.Common\Nuke.Common.csproj" />
<ProjectReference Include="..\..\src\Shims\Nuke.Build\Nuke.Build.csproj" />
<ProjectReference Include="..\..\src\Shims\Nuke.Components\Nuke.Components.csproj" />
</ItemGroup>

</Project>
Loading
Loading