Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .config/architecture-policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"qualifiedNameSeparator": ".",
"dependencyArrowMeaning": "A ProjectReference: the referencing project compiles against the referenced one."
},
"sourceRoots": ["src"],
"sourceRoots": ["src/src"],
"responsibilities": [
{
"name": "ArchitectureTesting",
Expand Down
8 changes: 4 additions & 4 deletions .config/purview-build.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"Build": {
"Solution": "src/ArchitectureTesting.slnx",
"TestRoot": "src/tests",
"TestRoot": "src",
"TestPatterns": "*Tests.csproj",
"TestFilter": "/*/*/*/*[Category=Unit]/"
},
Expand All @@ -13,8 +13,8 @@
"RequiredContent": {
"purview.architecture.testing": [
"README.md",
"lib/net10.0/Purview.Architecture.Testing.dll",
"lib/net10.0/Purview.Architecture.Testing.xml",
"lib/$(TFM)/Purview.Architecture.Testing.dll",
"lib/$(TFM)/Purview.Architecture.Testing.xml",
"content/architecture-policy.schema.json",
"purview-logo-light.png"
]
Expand All @@ -23,4 +23,4 @@
"Release": {
"Mode": "None"
}
}
}
24 changes: 17 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ derived test class. The repository also applies that package to itself.
| `src/src/ArchitectureTesting` | The package: `ArchitecturePolicy`, `ProjectGraph`, `AssemblyGraph`, `ArchitecturePolicyEvaluator`, `PurviewArchitectureTests` |
| `src/tests/ArchitectureTesting.UnitTests` | Rule tests against in-memory and fixture graphs, and the reader tests |
| `src/tests/Repository.UnitTests` | Repository-level assertions: the skill self-tests and the package applied to this repository |
| `src/samples/ArchitectureSample` | A small, fully working repository that adopts the package |
| `docs/` | The documentation site (MkDocs): getting started, policy reference, type-level rules, extending |
| `architecture-policy.schema.json` | The published policy schema, embedded into the package as a resource |
| `.config/architecture-policy.json` | This repository's own policy, exercised by the dogfood test |
| `skills/purview-architests` | The agent skill this repository publishes |
| `skills/` | The agent skills this repository publishes |
| `Directory.Packages.props` | Centrally managed NuGet versions |
| `src/Directory.Build.props` / `src/Directory.Build.targets` | Solution-wide SDK, package, analyzer, and build behavior |
| `global.json` | Required .NET SDK and Microsoft.Testing.Platform selection |
Expand Down Expand Up @@ -128,17 +130,25 @@ delivers skills through the skills CLI, which clones the *git* repository and on
`.agents/skills/` is the CLI's install location for skills from **other** repositories, and is
gitignored. This repository's own skills are never installed into it.

Install this repository's skill with:
Install this repository's skills with:

```bash
npx skills add purview-dev/architests
```

The single authored skill is `skills/purview-architests/`, which covers adopting, debugging and
extending `Purview.Architecture.Testing`. `src/tests/Repository.UnitTests/SkillTests.cs` validates it
on every test run through `Purview.Skills.Testing`'s rule set. The `[InheritsTests]` attribute on
that class is load-bearing — TUnit does not walk base types during discovery, so without it the run
is green and nothing is checked.
| Skill | Answers |
| -- | -- |
| `skills/purview-architests/` | adopting, configuring and debugging the package: the policy, the rules, waivers, the type-level rule, reading a failure |
| `skills/purview-architests-extending/` | adding a bespoke rule, the `ArchitectureContext`, and the package's internals |

Both carry the `purview-architests` prefix, because skills install into one flat directory per agent
shared with every skill a developer has installed from anywhere. `src/tests/Repository.UnitTests/SkillTests.cs`
validates them on every test run through `Purview.Skills.Testing`'s rule set, and asserts the prefix.
The `[InheritsTests]` attribute on that class is load-bearing — TUnit does not walk base types during
discovery, so without it the run is green and nothing is checked.

Keep the docs in step too: `README.md`, the package README, `docs/`, the schema and the skills all
describe the same behaviour.

## Completion checklist

Expand Down
4 changes: 2 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
<TUnitVersion>1.75.0</TUnitVersion>
<JsonSchemaVersion>9.4.0</JsonSchemaVersion>
<JsonSchemaVersion>9.4.1</JsonSchemaVersion>
</PropertyGroup>
<ItemGroup>
<!-- SourceLink is auto-added by the SDK to IsPackable projects. -->
Expand All @@ -21,6 +21,6 @@
<PackageVersion Include="TUnit.Mocks" Version="$(TUnitVersion)" />
<PackageVersion Include="Bogus" Version="35.6.5" />
<!-- The Agent Skills conformance rule set, shipped by Purview.BuildSdk. -->
<PackageVersion Include="Purview.Skills.Testing" Version="1.0.6" />
<PackageVersion Include="Purview.Skills.Testing" Version="1.0.7" />
</ItemGroup>
</Project>
77 changes: 69 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,92 @@

[![NuGet](https://img.shields.io/nuget/v/Purview.Architecture.Testing.svg)](https://www.nuget.org/packages/Purview.Architecture.Testing)
[![Release](https://github.com/purview-dev/architests/actions/workflows/release.yml/badge.svg)](https://github.com/purview-dev/architests/actions/workflows/release.yml)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE.md)

Enforces a repository's project and assembly dependency policy as TUnit tests, so the rules are
defined once rather than copied into every repository's test project.
Enforce a repository's architecture policy as TUnit tests, so the rules are defined once rather than
copied into every repository's test project.

Add a policy at `.config/architecture-policy.json`, add one derived test class, and the rules run:
dependency direction at both project and type level, sibling independence, composition boundaries,
forbidden packages and framework references, cycles, naming and test-reference checks.
- **Project-level dependency direction** — a declared `ProjectReference` the matrix forbids, or
does not permit.
- **Type-level dependency direction** — a type in one responsibility using a type in another,
including a dependency that only arrives transitively or through a package. Read from the compiled
assemblies by metadata only: nothing is loaded or executed.
- **Composition boundary** — only the composition root's category is exempt from being referenced
back to.
- **Sibling independence** — qualified implementations of a responsibility must not reference one
another.
- **Forbidden packages and framework references** — e.g. `Microsoft.AspNetCore.App` in a portable
layer.
- **Cycles, naming and test references** — the rest of the standard set, reported readably.
- **Waivers** — reviewed, per-project departures that keep every other rule in force and are
themselves checked.
- **An extension point** — add a bespoke rule to the same reporting, `SkipRule` and waiver model,
without a second architecture framework.

Adoption is two things: a policy file and one derived class.

```json
{
"$schema": "https://raw.githubusercontent.com/purview-dev/architests/main/architecture-policy.schema.json",
"version": 2,
"sourceRoots": ["src"],
"responsibilities": [
{
"name": "Core",
"aliases": ["Domain"],
"category": "domain",
"purpose": "The domain model, its invariants and its rules.",
"mayDependOn": [],
"mustNotDependOn": ["Application", "Persistence"],
"forbiddenFrameworkReferences": ["Microsoft.AspNetCore.App"]
}
]
}
```

```csharp
[InheritsTests]
public sealed class ArchitectureTests : PurviewArchitectureTests;
```

> **`[InheritsTests]` is required.** TUnit does not walk base types during discovery, so without it
> the class compiles, the run is green and nothing is enforced. TUnit's analyzer reports `TUnit0030`
> — treat it as an error.

## Install

```text
dotnet add package Purview.Architecture.Testing
```

See the [package README](src/src/ArchitectureTesting/README.md) for the full policy reference and
[`architecture-policy.schema.json`](architecture-policy.schema.json) for the schema.
## Documentation

- [Getting started](docs/Getting-Started.md) — adopt the package in a repository, step by step.
- [Policy reference](docs/Policy-Reference.md) — every field, every rule id, every default.
- [Type-level rules](docs/Type-Level-Rules.md) — how the assembly check works and how an assembly
is attributed to a project.
- [Extending](docs/Extending.md) — add your own rule to the standard set.
- [The policy schema](architecture-policy.schema.json) — the published JSON Schema.

## Sample

[`src/samples/ArchitectureSample`](src/samples/ArchitectureSample/README.md) is a small, fully
working repository that adopts the package: four projects, a policy, and a test project whose whole
suite is one derived class.

```bash
dotnet test src/samples/ArchitectureSample/tests/Sample.ArchitectureTests/Sample.ArchitectureTests.csproj
```

## Repository

- `src/src/ArchitectureTesting` — the package: policy reader, project and assembly graph, rule
evaluator, and the TUnit base class.
- `src/tests/ArchitectureTesting.UnitTests` — rule tests against in-memory and fixture graphs.
- `src/tests/Repository.UnitTests` — the skill self-tests and the package applied to this repository.
- `src/samples/ArchitectureSample` — the adoption sample.
- `architecture-policy.schema.json` — the published policy schema, embedded into the package.
- `skills/purview-architests` — the agent skill this repository publishes.
- `skills/` — the agent skills this repository publishes.

## Agent skills

Expand All @@ -43,7 +99,12 @@ npx skills add purview-dev/architests

```text
dotnet tool restore
just restore
just build
just test
just lint-check
just pack
just pipeline-pr
```

Part of [Purview.BuildSdk](https://github.com/purview-dev/build-sdk) · [purview.dev](https://purview.dev/projects/architests/)
66 changes: 66 additions & 0 deletions docs/Extending.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Extending

A repository can add a bespoke check without a second architecture framework. Implement
`IArchitectureRule` and return the same `ArchitectureViolation` type as the built-in rules:

```csharp
using Purview.Architecture.Testing;

[InheritsTests]
public sealed class ArchitectureTests : PurviewArchitectureTests
{
protected override IReadOnlyList<IArchitectureRule> AdditionalRules => [new NoConsoleLoggingRule()];

sealed class NoConsoleLoggingRule : IArchitectureRule
{
public string Rule => "noConsoleLogging";

public IReadOnlyList<ArchitectureViolation> Evaluate(ArchitectureContext context) =>
context.Projects.Values
.Where(project => !project.IsTestProject)
// ...inspect the project or its assembly...
.Select(project => new ArchitectureViolation(Rule, project.Name, "Console logging is banned here."))
.ToArray();
}
}
```

## What the context gives you

`ArchitectureContext` carries the three inputs a rule can need:

| Member | What it is |
| -- | -- |
| `Policy` | The parsed `ArchitecturePolicy`, so a rule reads the same definition the built-in rules do. |
| `Projects` | The project graph, keyed by project name, with declared references, packages, framework references and analyzers. |
| `Assemblies` | The compiled assemblies, keyed by project name, each with the external types it references. Empty when `assemblyRules.enabled` is false. |

## What you get for free

An additional rule participates in everything the built-in rules do:

- **Its own `[Test]`.** Each rule id becomes a case of
`AdditionalRule_GivenTheRepository_RespectsThePolicy`, so a failure names the rule.
- **`SkipRule`.** Override `SkipRule` and return `true` for the rule's `Rule` id.
- **Waivers.** A waiver whose `rules` names the rule's id is honoured automatically, so a reviewed,
per-project departure needs no code.
- **The same reporting.** `ArchitecturePolicyEvaluator.Report` renders the violations, and the
failure's `.Because(...)` lists every one.

## Keep the rule in a fixture

The evaluator takes an in-memory graph, so a rule is testable as a small fixture rather than a
repository on disk:

```csharp
var evaluator = new ArchitecturePolicyEvaluator(
policy,
projects,
assemblies,
[new NoConsoleLoggingRule()]
);

await Assert.That(evaluator.EvaluateAdditionalRule("noConsoleLogging")).IsEmpty();
```

That is exactly how the package's own rules are tested.
Loading
Loading