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
3,881 changes: 3,881 additions & 0 deletions .config/api-surface.json

Large diffs are not rendered by default.

33 changes: 32 additions & 1 deletion .config/architecture-policy.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/purview-dev/architests/main/architecture-policy.schema.json",
"version": 2,
"version": 3,
"description": "The architecture policy this repository applies to itself, through the package it ships.",
"conventions": {
"qualifiedNameSeparator": ".",
Expand All @@ -14,6 +14,13 @@
"purpose": "The architecture rule engine: policy reading, the project and assembly graph, and the TUnit rule set.",
"mayDependOn": [],
"mustNotDependOn": []
},
{
"name": "Architecture",
"category": "host",
"purpose": "The architests command-line host that runs the evaluator outside TUnit.",
"mayDependOn": ["ArchitectureTesting"],
"mustNotDependOn": []
}
],
"discouragedProjectNames": {
Expand All @@ -22,5 +29,29 @@
},
"assemblyRules": {
"enabled": true
},
"typeRules": {
"enabled": true,
"appliesTo": {
"categories": ["shared", "host"]
},
"excludeGeneratedCode": true
},
"apiRules": {
"enabled": true
},
"portRules": {
"enabled": true
},
"dependencyBudget": {
"enabled": true,
"failOnStale": true,
"entries": []
},
"policyIntegrity": {
"checkReachability": true
},
"waiverPolicy": {
"maxAgeDays": 180
}
}
4 changes: 2 additions & 2 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@
"rollForward": false
},
"purview.build": {
"version": "0.5.4",
"version": "0.7.1",
"commands": [
"purview-build"
],
"rollForward": false
}
}
}
}
7 changes: 6 additions & 1 deletion .config/purview-build.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"Solution": "src/ArchitectureTesting.slnx",
"TestRoot": "src",
"TestPatterns": "*Tests.csproj",
"TestFilter": "/*/*/*/*[Category=Unit]/"
"TestFilter": "/*/*/*/*/"
},
"PackValidation": {
"RequiredCompilerFlags": [
Expand All @@ -17,6 +17,11 @@
"lib/$(TFM)/Purview.Architecture.Testing.xml",
"content/architecture-policy.schema.json",
"purview-logo-light.png"
],
"purview.architecture.testing.cli": [
"README.md",
"purview-logo-light.png",
"tools/net10.0/any/*"
]
}
},
Expand Down
2 changes: 0 additions & 2 deletions .csharpierignore

This file was deleted.

26 changes: 21 additions & 5 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -1,3 +1,23 @@
# Purview.BuildSdk 1.1.0 - managed file, do not edit
#
# This file is written by Purview.BuildSdk and is replaced whenever the SDK version changes. The version
# on the first line is how the SDK recognises a file it owns: a .editorconfig without that marker is
# yours, and is never touched.
#
# To add your own rules, create a second .editorconfig in a subdirectory - `src/.editorconfig` is the
# usual choice - without `root = true`. EditorConfig applies the nearest file last, so your settings win
# over these, and nothing you write is lost when this file is updated.
#
# To stop the SDK updating this file, set one of:
# <PurviewEditorConfigAutoUpdate>false</PurviewEditorConfigAutoUpdate> keep this file as it is
# <PurviewRepoBootstrapMode>Never</PurviewRepoBootstrapMode> opt out of all bootstrapping
# <BootstrapEditorConfigToRepoRoot>false</BootstrapEditorConfigToRepoRoot>
#
# Deleting the first line also works: without the marker the SDK treats the file as yours. You then own
# every rule in it, including the ones a future SDK version would have fixed.
#
# See https://github.com/purview-dev/build-sdk#editorconfig for the full description.

root = true

# Base Configuration - General Settings, File Formatting, and Naming Conventions
Expand Down Expand Up @@ -360,12 +380,8 @@ dotnet_style_require_accessibility_modifiers = omit_if_default:warning
# Code-block preferences
csharp_prefer_braces = when_multiline:suggestion
csharp_prefer_simple_using_statement = false:silent
csharp_prefer_system_threading_lock = true
dotnet_style_prefer_collection_expression = true:suggestion

# Enable foreach explicit cast preference
dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed:warning

# Prefer System.Threading.Lock
csharp_prefer_system_threading_lock = true:warning

Expand Down Expand Up @@ -497,7 +513,7 @@ dotnet_style_prefer_auto_properties = true:warning
dotnet_style_prefer_compound_assignment = true:warning
dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
dotnet_style_prefer_conditional_expression_over_return = true:suggestion
dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed
dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed:warning
dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning
dotnet_style_prefer_inferred_tuple_names = true:warning
dotnet_style_prefer_simplified_boolean_expressions = false:silent
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ on:
push:
branches: [main]

permissions:
contents: write
# The shared release workflow pushes the same packages to nuget.org and, when
# PURVIEW_PUBLISH_GITHUB_PACKAGES=true, to GitHub Packages. A reusable workflow cannot grant
# more than its caller, so the permission is declared here.
packages: write

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
Expand Down
45 changes: 39 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@ derived test class. The repository also applies that package to itself.
| Path | Purpose |
| --- | --- |
| `src/ArchitectureTesting.slnx` | Canonical solution for restore, build, test, and pack |
| `src/src/ArchitectureTesting` | The package: `ArchitecturePolicy`, `ProjectGraph`, `AssemblyGraph`, `ArchitecturePolicyEvaluator`, `PurviewArchitectureTests` |
| `src/src/ArchitectureTesting` | The package: `ArchitecturePolicy`, `ProjectGraph`, `AssemblyGraph`, `APISurface`, `ArchitecturePolicyEvaluator`, `PurviewArchitectureTests` |
| `src/src/Architecture.CLI` | The `architests` dotnet tool: `validate`, `waivers list`; assembly and command are `architests`, package ID is `Purview.Architecture.Testing.Cli` |
| `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/tests/ArchitectureTesting.CLI.UnitTests` | The tool's parser, exit codes, `validate` and `waivers list` |
| `src/tests/Repository.UnitTests` | Repository-level assertions: the skill self-tests, the rule/docs drift test, 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 |
| `docs/` | The documentation site (MkDocs): getting started, policy reference, type-level rules, command line, 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 |
| `.config/api-surface.json` | The committed public API baseline, compared by `apiSurface` and `architests api check` |
| `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 |
Expand All @@ -40,7 +43,8 @@ derived test class. The repository also applies that package to itself.
2. Confirm behavior from code and tests rather than relying on memory or documentation alone.
3. Make the smallest coherent change. Preserve public behavior unless the task explicitly changes it.
4. Update tests for fixes and behavior changes. Update documentation when public behavior changes.
5. Run the narrowest meaningful validation first, then broader validation in proportion to risk.
5. Run the narrowest meaningful validation first — `architests validate` for a policy change — then
broader validation in proportion to risk.
6. Review the diff for unrelated edits, generated noise, compatibility risks, and missing docs or tests.

## The three-layer design (non-negotiable)
Expand All @@ -57,6 +61,23 @@ Keep the layers separated, because the separation is what makes the rules testab
Every new rule must be evaluable from an in-memory fixture and asserted by a small
`ArchitectureTesting.UnitTests` test, exactly as the existing rules are.

## Dogfooding (non-negotiable)

- The package applies every rule it ships to this repository, through
`.config/architecture-policy.json` and `src/tests/Repository.UnitTests/ArchitectureTests.cs`.
- A change that adds a rule enables it in the dogfood policy in the same commit. A rule that cannot
pass here is not ready to ship.
- The repository is clean under every enabled rule: fix the violation, or record a waiver with a
reason and a review date. `SkipRule` is not used in the dogfood test.
- The dogfood test project references every production project, so the type-level and surface
readers see each assembly in its output directory. A production project missing from that reference
list escapes every metadata rule silently.
- The rule catalogue, `docs/`, both READMEs and the skills must list every rule id;
`RuleDocumentationTests` fails on drift. Add the id to the catalogue and to the documents in the
same change.
- Public API baseline changes, once a baseline exists, are ordinary reviewed changes: deliberate, in
the same commit, and explained in the pull request.

## Reading assemblies

- Read compiled assemblies by **metadata only** (`System.Reflection.Metadata`). Never `Assembly.Load`,
Expand Down Expand Up @@ -120,6 +141,10 @@ Use `dotnet csharpier check .` for validation and `dotnet csharpier format .` to
pipeline with `Release:Mode=NuGet`.
- Never create release tags or publish packages manually unless the user explicitly requests a
documented recovery procedure.
- The release version is whatever `package.json` says; never infer it from this document, and do not
bump it as part of an unrelated change.
- The architecture policy document version is **3**. Bump it when a change would break a reader that
understood the previous shape; adding optional sections does not require a bump.

## Agent skills

Expand All @@ -139,9 +164,13 @@ npx skills add purview-dev/architests
| 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-policy-authoring/` | writing and repairing the policy: responsibilities, the matrix, policy integrity, waiver expiry |
| `skills/purview-architests-type-rules/` | enabling and debugging `typeRules`: naming, shape, namespace, attributes, and which public types are governed |
| `skills/purview-architests-contracts/` | the committed API baseline, documentation requirements, and the port rules |
| `skills/purview-architests-migration/` | adopting incrementally in an existing repository, and ratcheting a policy tighter |
| `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
All 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
Expand All @@ -158,7 +187,11 @@ Before handing work back:
- Confirm only intended files changed.
- Review public API, schema, and package-content implications.
- Add or update focused tests for code changes.
- Update `README.md`, the package README, the skill and the schema when public behavior changes.
- Update `README.md`, the package README, the skills and the schema when public behavior changes;
the rule catalogue and the documents are checked for drift by `RuleDocumentationTests`.
- Add every new rule to `ArchitectureRules.Catalogue` and enable it in the dogfood policy.
- Run `architests validate`, `architests waivers list --stale` and `architests api check` against this
repository. Regenerate `.config/api-surface.json` only for an intended public API change.
- Run appropriate build, test, formatting, and pack checks in proportion to risk.
- State exactly what validation ran. If a check was skipped or blocked, give the concrete reason and
remaining risk.
12 changes: 2 additions & 10 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -1,26 +1,18 @@
<Project>
<PropertyGroup>
<!-- Set explicitly rather than relying on auto-detection, so projects outside src/ (samples)
also use central package management. -->
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
<TUnitVersion>1.75.0</TUnitVersion>
<TUnitVersion>1.76.0</TUnitVersion>
<JsonSchemaVersion>9.4.1</JsonSchemaVersion>
</PropertyGroup>
<ItemGroup>
<!-- SourceLink is auto-added by the SDK to IsPackable projects. -->
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.402" />
<!-- Validates architecture-policy.json against the schema embedded in Purview.Architecture.Testing. -->
<PackageVersion Include="JsonSchema.Net" Version="$(JsonSchemaVersion)" />
</ItemGroup>
<ItemGroup Label="Testing">
<PackageVersion Include="TUnit" Version="$(TUnitVersion)" />
<PackageVersion Include="TUnit.Core" Version="$(TUnitVersion)" />
<PackageVersion Include="TUnit.Assertions" Version="$(TUnitVersion)" />
<!-- Auto-added to test projects by the SDK. -->
<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.7" />
<PackageVersion Include="Purview.Skills.Testing" Version="1.1.0" />
</ItemGroup>
</Project>
50 changes: 48 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ copied into every repository's test project.
another.
- **Forbidden packages and framework references** — e.g. `Microsoft.AspNetCore.App` in a portable
layer.
- **Type naming and shape** — interface, attribute and exception conventions; sealed classes,
read-only collections, root namespaces and attributes, read from metadata.
- **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.
Expand All @@ -29,7 +31,7 @@ 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,
"version": 3,
"sourceRoots": ["src"],
"responsibilities": [
{
Expand Down Expand Up @@ -60,13 +62,54 @@ public sealed class ArchitectureTests : PurviewArchitectureTests;
dotnet add package Purview.Architecture.Testing
```

The same policy can be checked outside a test run:

```bash
dotnet tool install --global Purview.Architecture.Testing.Cli
architests validate
architests waivers list --stale
```

Exit codes are part of the contract: `0` clean, `1` findings, `2` a malformed command line.

## Rules

| Rule id | Catches |
| -- | -- |
| `policyIntegrity` | A policy that names an unknown responsibility, permits and forbids the same dependency, or declares a duplicate name or alias |
| `graphDiscovery` | No production projects discovered, so every other rule would pass vacuously |
| `naming` | A project name that maps to no responsibility or alias |
| `discouragedName` | A project named after a category rather than a responsibility |
| `dependencyMatrix` | A declared `ProjectReference` the matrix forbids or does not permit |
| `assemblyDependencyMatrix` | A type-level reference the matrix forbids or does not permit, including one that arrives transitively |
| `typeNaming` | A public interface without `I`, an attribute or exception without its suffix, or a discouraged suffix such as `Manager` |
| `typeShape` | An unsealed public class, a public field, a mutable collection return, or a utility class that is not static |
| `typeNamespace` | A public type declared outside its project's root namespace |
| `attributePolicy` | A missing or forbidden attribute, `[Obsolete]` without a message, or `InternalsVisibleTo` naming production code |
| `apiSurface` | The public API differs from the committed baseline |
| `apiDocumentation` | A public type or member has no XML documentation |
| `portImplementation` | A port in an inner layer has no implementation |
| `portPurity` | A port exposes an infrastructure technology |
| `dependencyBudget` | A project references beyond its budget, or the budget still allows a reference that has gone |
| `siblingIndependence` | A qualified implementation referencing a qualified sibling |
| `compositionBoundary` | A non-composition, non-host project referencing the composition root |
| `forbiddenPackage` | A package the responsibility owns matching a forbidden prefix |
| `forbiddenFramework` | A framework reference matching a forbidden prefix for the responsibility |
| `cycle` | A cycle in the project graph |
| `testReference` | A production project referencing a test project |
| `waiverIntegrity` | A stale, unjustified or malformed waiver, or one excusing an unknown or unwaivable rule |

## Documentation

- [Getting started](docs/Getting-Started.md) — adopt the package in a repository, step by step.
- [Tutorial](docs/Tutorial.md) — grow one application and its policy from a two-project matrix to type, API, port and budget rules.
- [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.
- [API contracts](docs/Contracts.md) — the committed baseline, XML documentation and the port rules.
- [Migration and budget](docs/Migration.md) — adopt incrementally and ratchet the project graph.
- [Extending](docs/Extending.md) — add your own rule to the standard set.
- [Command line](docs/Cli.md) — validate, capture the API, review waivers and render the graph.
- [The policy schema](architecture-policy.schema.json) — the published JSON Schema.

## Sample
Expand All @@ -83,8 +126,11 @@ dotnet test src/samples/ArchitectureSample/tests/Sample.ArchitectureTests/Sample

- `src/src/ArchitectureTesting` — the package: policy reader, project and assembly graph, rule
evaluator, and the TUnit base class.
- `src/src/Architecture.CLI` — the `architests` dotnet tool.
- `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/tests/ArchitectureTesting.CLI.UnitTests` — the tool's parser and commands.
- `src/tests/Repository.UnitTests` — the skill self-tests, the rule/documentation drift test, 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/` — the agent skills this repository publishes.
Expand Down
Loading
Loading