Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
118 commits
Select commit Hold shift + click to select a range
65a6bb0
build(skills): added purview skills
kieronlanning Oct 27, 2026
2d340b8
feat(core): typed error paths, structured bounds and never-throw mess…
kieronlanning Oct 27, 2026
7b65d90
feat(core): member validation, member resolution, untyped validators …
kieronlanning Oct 27, 2026
878ece1
fix(generator): [Required] rejects null, empty and whitespace strings…
kieronlanning Oct 27, 2026
e88684a
fix(json): keep JSON error locations, one invalid_json code and full …
kieronlanning Oct 27, 2026
3a6b6b4
perf(generator): add generated-validation benchmarks
kieronlanning Oct 27, 2026
7436f82
style: format array and tuple tests
kieronlanning Oct 27, 2026
9072c41
fix(schemas): structured, consistent errors from runtime schemas and …
kieronlanning Oct 27, 2026
092ae52
Merge branch 'worktree-agent-ac3861b6f0bb37148' into net11-extensions
kieronlanning Oct 27, 2026
b671979
feat(core): generated-validator helpers (depth guard, member info, re…
kieronlanning Oct 27, 2026
ebf1134
feat(openapi): add Purview.ZodSharp.AspNetCore.OpenApi schema, operat…
kieronlanning Oct 27, 2026
674152f
chore: merge net11-extensions (runtime schemas, rules and JSON error …
kieronlanning Oct 27, 2026
190d0eb
Merge branch 'net11-extensions' into worktree-agent-a6ca9ef9f57cedc1a
kieronlanning Oct 27, 2026
a7e9d97
feat(options): async options validation and path-bearing failures
kieronlanning Oct 27, 2026
22eaaea
feat(aspnetcore): one ProblemDetails writer, endpoint filter factory …
kieronlanning Oct 27, 2026
95369e9
feat(validation): add Purview.ZodSharp.Extensions.Validation for Micr…
kieronlanning Oct 27, 2026
2f5898b
Merge branch 'worktree-agent-ae7d3c14afef1ee7e' into net11-extensions
kieronlanning Oct 27, 2026
6acb3da
fix(validation): align formatter parameter name with IZodMessageForma…
kieronlanning Oct 27, 2026
1ce9a97
feat(openapi): describe unions, required members and error contract; …
kieronlanning Oct 27, 2026
1d58ee8
Merge branch 'worktree-agent-a6ca9ef9f57cedc1a' into net11-extensions
kieronlanning Oct 27, 2026
f404723
build: remove duplicate TestHost package version after merge
kieronlanning Oct 27, 2026
c386294
feat(generator): per-member validation, typed paths, runtime messages…
kieronlanning Oct 27, 2026
e1e12c1
feat(aspnetcore): full-fidelity ProblemDetails from the .NET 11 valid…
kieronlanning Oct 27, 2026
bd96878
fix(options): keep the 2.4.0 AddZodSchemaValidator overload binary co…
kieronlanning Oct 27, 2026
4f4e98a
fix(validation): validate C# union parameters without an application …
kieronlanning Oct 27, 2026
a98165f
feat(blazor): add Purview.ZodSharp.Blazor (ZodValidator, message comp…
kieronlanning Oct 27, 2026
522aed2
feat(aspnetcore): add AddZodSharpClientValidation for static-SSR ZodS…
kieronlanning Oct 27, 2026
46a5556
feat(generator): new diagnostics, ErrorType origin, trim-safe validat…
kieronlanning Oct 27, 2026
5191204
style: format net11 generator tests
kieronlanning Oct 27, 2026
86f7adb
docs: changelog entries for the source generator overhaul
kieronlanning Oct 27, 2026
a83c498
Merge branch 'worktree-agent-a5f05ef54062b5dd2' into net11-extensions
kieronlanning Oct 27, 2026
3c4c484
test(blazor): cover both validator routes, problem-details mapping an…
kieronlanning Oct 27, 2026
81ec25a
Merge branch 'net11-extensions' into blazor-work
kieronlanning Oct 27, 2026
96a47ca
fix(aspnetcore): apply generated schema registrations; align the dept…
kieronlanning Oct 27, 2026
c841e36
test(blazor): static SSR, client-validation contract, generated model…
kieronlanning Oct 27, 2026
941e752
docs(blazor): package README, wiki page, skill and changelog for Purv…
kieronlanning Oct 27, 2026
69bdf55
build(blazor): declare the static web asset and build files in pack v…
kieronlanning Oct 27, 2026
01f0031
Merge branch 'blazor-work' into net11-extensions
kieronlanning Oct 27, 2026
8275439
feat(samples): add the Examples.MinimalApi Native AOT sample
kieronlanning Oct 27, 2026
67c5d8c
ci: publish the minimal API sample with Native AOT and smoke-test it
kieronlanning Oct 27, 2026
f9e366b
Merge branch 'samples-work' into net11-extensions
kieronlanning Oct 27, 2026
d7691df
docs: update wiki, skills and package READMEs for the .NET 11 validat…
kieronlanning Oct 27, 2026
e4ae152
Merge branch 'net11-extensions' into docs-work
kieronlanning Oct 27, 2026
bf49630
style: format the minimal API sample
kieronlanning Oct 27, 2026
6c66d9c
fix(aspnetcore): per-issue parameters by default and request-bound va…
kieronlanning Oct 27, 2026
5769510
Merge branch 'docs-work' into net11-extensions
kieronlanning Oct 27, 2026
75c6cfa
docs: per-issue parameters default and request-bound validation reports
kieronlanning Oct 27, 2026
2978c26
feat(trim): annotate the Newtonsoft.Json integration and mark it trim…
kieronlanning Oct 27, 2026
5b1ca0a
feat(trim): add trim-safe [MinCount]/[MaxCount]/[CountRange] collecti…
kieronlanning Oct 27, 2026
02b62b5
feat(trim): add ValidateWithZod, the trim-safe counterpart of BindAnd…
kieronlanning Oct 27, 2026
6f6b270
fix(trim): declare the trim/AOT requirements of static-SSR client val…
kieronlanning Oct 27, 2026
ef2c1ab
feat(trim): guard reflection fallbacks with the ZodSharp.Reflection.I…
kieronlanning Oct 27, 2026
0767079
fix(trim): limit the endpoint filter's dynamic-code suppression to ne…
kieronlanning Oct 27, 2026
a54dff5
test(trim): add the Native AOT trim-safety verification harness
kieronlanning Oct 27, 2026
6c7b342
ci: publish and run the trim-safety harness with Native AOT
kieronlanning Oct 27, 2026
578a61d
test: track ZODSGEN053 as an unshipped analyzer rule
kieronlanning Oct 27, 2026
3c9c75e
docs: add Trimming and Native AOT guidance
kieronlanning Oct 27, 2026
49ded65
ci: give the verify-trim recipe a one-line description
kieronlanning Oct 27, 2026
ea72b3d
style: exclude the harness union model from CSharpier
kieronlanning Oct 27, 2026
bf92358
style: format with the vendored CSharpier used by just lint-check
kieronlanning Oct 27, 2026
ee0946d
fix(generator): read MSBuild switches with the build_property. key
kieronlanning Oct 27, 2026
3c0ff90
fix(rules): keep the input out of number defaults and report rule tem…
kieronlanning Oct 27, 2026
531b016
chore: remove empty files added at the repository root by mistake
kieronlanning Oct 27, 2026
97234a1
fix(rules): resolve displayName at the root so Required's template fo…
kieronlanning Oct 27, 2026
4f203cd
fix(rules): report SafeIntegerRule's code by direction
kieronlanning Oct 27, 2026
c38d25d
test(options): prove AddZodSchemaOptionsValidator runs async rules on…
kieronlanning Oct 27, 2026
afbd7ed
fix(generator): qualify schema hint names so same-named types generate
kieronlanning Oct 27, 2026
c5d8cff
fix(generator): report custom validation parameter errors accurately
kieronlanning Oct 27, 2026
e94c58b
docs: fix the ZodAsyncMember and options validation XML-doc examples
kieronlanning Oct 27, 2026
78407df
feat(aspnetcore): accept any error sequence in ZodProblemDetailsWrite…
kieronlanning Oct 27, 2026
f66361d
fix(openapi): describe ZodValidationIssue code and category correctly
kieronlanning Oct 27, 2026
bbaa019
fix(openapi): write enum values as System.Text.Json writes them
kieronlanning Oct 27, 2026
c7c66a0
fix(generator): report number [Range] bounds as typed parameters
kieronlanning Oct 27, 2026
db22f34
fix(json): keep converter read-failure paths and name more expected t…
kieronlanning Oct 27, 2026
15ab32e
fix(generator): report array/record as the origin of collection sizes
kieronlanning Oct 27, 2026
f14cd26
fix(json): resolve option-less calls when both JSON packages are refe…
kieronlanning Oct 27, 2026
81a9690
build!: drop net8.0 and net9.0
kieronlanning Oct 27, 2026
a73fee6
chore(release): 3.0.0-prerelease.1
kieronlanning Oct 27, 2026
63e4497
chore(release): name the 3.0.0-prerelease.1 changelog and ship analyz…
kieronlanning Oct 27, 2026
2759e9a
docs: supported target frameworks are net10.0 and net11.0
kieronlanning Oct 27, 2026
dbd939f
style: format the compatibility suppressions with the repository form…
kieronlanning Oct 27, 2026
448be1c
build(commitlint): accept the ! breaking-change marker in headers
kieronlanning Oct 27, 2026
c835f2a
feat(samples): add a Blazor Web App sample with a WebAssembly client
kieronlanning Oct 27, 2026
f2f8a71
test: cover the Blazor sample in memory and in a browser
kieronlanning Oct 27, 2026
53eb3a8
ci: verify the Blazor WebAssembly client trimmed, AOT-compiled and in…
kieronlanning Oct 27, 2026
e2099a4
docs(blazor): document the Blazor sample and per-keystroke input for …
kieronlanning Oct 27, 2026
d994524
build(deps): bump Purview.SourceGeneratorFramework to 1.2.2 and drop …
kieronlanning Oct 27, 2026
2a69f72
build(lint): point the pipeline at the vendored CSharpier
kieronlanning Oct 27, 2026
757b61e
build(pack): forbid compiler-host content outside analyzers in every …
kieronlanning Oct 28, 2026
441d5d1
fix(openapi): flow Microsoft.AspNetCore.OpenApi build assets to consu…
kieronlanning Oct 28, 2026
6900b66
ci: gate releases on packed consumers and a compiler-host leak scan
kieronlanning Oct 28, 2026
e10d71c
test: guard the release gates with ReleaseGateTests
kieronlanning Oct 28, 2026
2afab9b
test(trim): record why the rooted publish still tolerates IL3054
kieronlanning Oct 28, 2026
9b0de1f
fix(openapi): enable the OpenAPI generator interceptors through build…
kieronlanning Oct 28, 2026
bf8ef22
ci: scan project bins in the local publish recipes and clarify the ru…
kieronlanning Oct 28, 2026
ef504a0
fix: define [ZodSchema] and [ErrorType] in Purview.ZodSharp instead o…
kieronlanning Oct 28, 2026
e81a692
test(packed): add a multi-project OpenAPI consumer to the packed-cons…
kieronlanning Oct 28, 2026
7a2a912
build(deps): bump Purview.BuildSdk and Purview.Skills.Testing to 1.2.0
kieronlanning Oct 28, 2026
5f6ee3b
fix(generator): test for null with patterns so member operators never…
kieronlanning Oct 28, 2026
527f012
fix(generator): pass a rule identity by direct cast so wasm aot compiles
kieronlanning Oct 28, 2026
7d8545b
fix(blazor): place errors inside struct members on the owning field
kieronlanning Oct 28, 2026
8e493ee
fix(aspnetcore): deprecate the option-less problemdetails overloads
kieronlanning Oct 28, 2026
4e242dc
docs: explain the cp0001 a library sees for the removed embedded attr…
kieronlanning Oct 28, 2026
9c8b7ea
fix(blazor): stop addzodsharpblazor from turning on minimal-api valid…
kieronlanning Oct 28, 2026
29611ae
fix(aspnetcore): keep declaringtype and member out of problemdetails …
kieronlanning Oct 28, 2026
bd553bc
docs: explain ca1849 on sync validate calls and say when it is noise
kieronlanning Oct 28, 2026
c028b8b
docs(skills): reword the webassembly aot note on type-level rules
kieronlanning Oct 28, 2026
77ed187
chore(skills): keep installed skills under .agents only and drop the …
kieronlanning Oct 28, 2026
ef6bd64
docs: describe the committed .agents skills and how to update them
kieronlanning Oct 28, 2026
332dd9d
docs: state that this repository's own skills are for consumers, neve…
kieronlanning Oct 28, 2026
930e0bf
build: bumped shared build
kieronlanning Oct 28, 2026
98a9e61
feat: added a supressor for CA1849 when using Schema Validate vs. Val…
kieronlanning Oct 28, 2026
4b950f4
fix(gen): struct collections where being compared to null
kieronlanning Oct 29, 2026
3ae3f47
chore: removed old docs
kieronlanning Oct 29, 2026
040ac20
feat: added .Http package for final Aot/ Browser compatible release
kieronlanning Nov 2, 2026
d043117
chore: updated skills
kieronlanning Nov 2, 2026
bd8ed53
fix(ci): exit 0 from pack-rule proof so a rejection run's exit code d…
kieronlanning Nov 2, 2026
4af13d6
fix: better integration for strict vs. non-strict in aot
kieronlanning Nov 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
183 changes: 183 additions & 0 deletions .agents/skills/purview-architecture-testing/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
---
name: purview-architecture-testing
description: >-
Use when adding, changing or debugging architecture tests in a .NET solution - dependency-direction
rules, forbidden framework references, project naming checks, cycle detection - or when a boundary
keeps being broken in review and should fail the build instead. Adopts Purview.Architecture.Testing
and a single policy file rather than hand-maintained lists.
license: MIT
metadata:
author: Purview-Dev
version: "2.0"
---

# Purview architecture testing

A boundary that only review enforces is a boundary that erodes. The job of this skill is to turn a
rule a reviewer keeps repeating into a test that fails the build, **without** producing a wall of
brittle assertions that everyone disables six months later.

Prerequisite: know what the projects are *for*. See `purview-architecture`.

## 1. Only test a rule that is real

Before writing anything, answer: **has this rule been broken, or is it certain to be?**

A test for a dependency nobody would plausibly add is cost with no benefit. Good candidates:

- A direction that has already been violated once.
- A boundary that is load-bearing - Core staying free of EF Core, Composition being referenced by
nothing.
- A rule a newcomer cannot infer from the code.

Bad candidates: anything the compiler already enforces, and style preferences dressed up as
architecture.

## 2. Adopt the package; define the policy once

The failure mode of architecture tests is a dependency matrix written twice - once in a wiki table and
once in assertions - which then disagree. The prose is the copy that drifts, because nothing fails
when it is wrong.

So do not write the rule engine. `Purview.Architecture.Testing` ships it, so the matrix, naming,
sibling-independence, composition, forbidden-package, cycle, test-reference and waiver rules are
defined once and shared. Adoption is two files.

**A policy** at `.config/architecture-policy.json`, pointing at the published schema so an editor
validates it as you type:

```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": ["Shared"],
"mustNotDependOn": ["Application", "Persistence", "Composition"],
"forbiddenPackagePrefixes": ["Microsoft.EntityFrameworkCore", "Microsoft.AspNetCore"]
}
],
"waivers": []
}
```

**One derived test class:**

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

That is the whole of the adoption. Each rule is a separate test, so a failure names the rule rather
than collapsing the set into one assertion, and the package validates the policy against its schema
first - a policy that does not match the schema may be silently missing a rule.

The build-sdk ships a skill for the package itself, `purview-buildsdk-architecture-tests`, which is
authoritative on its API and rule ids. Read it rather than this page when you are extending or
debugging the package.

Then add a test that the documentation matches the policy, so the table in the wiki is falsifiable
too. Full worked example:
[Project Responsibilities](https://github.com/purview-dev/engineering-playbook/blob/main/docs/wiki/Project-Responsibilities.md).

## 3. Read project files, and assemblies

Two sources, because they catch different things:

| Source | Catches | Misses |
| -- | -- | -- |
| `.csproj` `ProjectReference` | The reference a reviewer actually added | A dependency arriving transitively |
| Compiled assembly references | A dependency that arrived indirectly | Intent — it cannot tell direct from transitive |

The package's rules read the **project files**, because a declared `ProjectReference` is what a
reviewer adds and what the policy governs. Add a small hand-written test that reads the **compiled
assemblies** for the leak a project file cannot show - a namespace from the wrong layer reaching a
public type.

Classify a project by the **first segment** of its name, so `Persistence.SqlServer` and `Persistence`
are both Persistence - qualification is a naming choice, never a different rule. The package does
this for you; a hand-written assembly test must do it too.

## 4. The rules worth having

| Rule | What it catches |
| -- | -- |
| Dependency matrix | Core referencing Persistence; Application referencing a concrete provider |
| Nothing references Composition | A circular implementation choice |
| Sibling independence | `Providers.GitHub` reaching into `Providers.AzureDevOps` |
| Forbidden package prefixes | EF Core in Core; ASP.NET hosting in a domain project |
| Cycle detection | A graph that MSBuild rejects with an unreadable message |
| Catch-all names | `Infrastructure`, `Services`, `Helpers` appearing as project names |
| No production → test reference | A test helper leaking into shipped code |

The package provides all of these. Do not re-implement one; change the policy instead.

## 5. Write failures someone can act on

This is what decides whether the suite survives. A failure must name:

1. The offending project.
2. The rule violated.
3. The specific reference that broke it.
4. The direction the dependency *should* run.
5. The likely fix.

```text
'Application' (Application) references 'Persistence.SqlServer' (Persistence), but Application
must not depend on Persistence. Invert it: Persistence may depend on Application, or introduce
an abstraction that Application owns and Persistence implements.
```

Not `Expected collection to be empty`. Collect every violation and report them as a numbered list -
one test run should show all of them, not the first. The package already reports this way; keep the
same standard in any rule you add.

## 6. Waivers, not suppressions

A repository that predates the policy will violate it. Deleting the test or adding a blanket
`#pragma` makes the policy a lie. Record the departure in the policy instead:

```json
{
"project": "Infrastructure",
"mapsTo": "Adapters",
"rules": ["naming"],
"reason": "Holds one adapter, predates the catalogue; renaming churns the wiki and docs for no gain.",
"reviewedOn": "2026-10-15"
}
```

The package tests the waivers themselves: each must name a project that still exists, a rule, and a
substantive reason. A waiver for a deleted project is dead weight that hides the next one.

Critically, a waiver excuses **the narrowest thing possible** - usually the name. `mapsTo` keeps the
project classified so every dependency rule still applies to it. Prefer a waiver to
`SkipRule`, which removes a rule for the whole repository with no record.

## 7. Conventions

- TUnit, matching the repository's other tests, adopted through `PurviewArchitectureTests` with
`[InheritsTests]`.
- Arrange/Act/Assert, one behaviour per test.
- Let the package find the repository root - it probes upward for the policy and stops at the `.git`
boundary. Override `RepositoryRoot` only when the policy does not sit above the test binary.
- Keep the policy file free of anything a rule cannot read; a field no rule consults is a rule that
looks enforced and is not.

## Checks before you answer

- Is every rule you added one that has been, or will be, broken?
- Is the matrix defined once, in the policy, with the documentation checked against it?
- Does each failure name the fix, not just the fact?
- Did you record a waiver rather than weaken a rule for existing code?

## See also

- `purview-architecture` — what the projects are for, and how they are named
- `purview-buildsdk-architecture-tests` — the package's own skill, authoritative on its API
- [Architecture as Tests](https://github.com/purview-dev/engineering-playbook/blob/main/docs/wiki/Architecture-As-Tests.md)
176 changes: 176 additions & 0 deletions .agents/skills/purview-architecture/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
---
name: purview-architecture
description: >-
Use when deciding the shape of a .NET solution that uses Purview libraries — how many projects,
whether to add a repository or CQRS layer, where validation lives, how an API and its clients share
a contract (Contracts, OpenAPI or Hybrid) and when a BFF is earned — or before generating
substantial implementation code. Establishes project context first, then applies DRY, KISS and
architecture proportional to the actual requirements rather than to an enterprise reference diagram.
license: MIT
metadata:
author: Purview-Dev
version: "1.0"
---

# Purview architecture

The job of this skill is to stop two failures that cost far more than they look:

- **Architecture bought in advance.** Four projects, a repository interface and a mapping layer for an
application that has one table and one caller.
- **A recommendation made without context.** Advice about database-side GUID generation given to a
stateless CLI, or a Clean Architecture split proposed for a 300-line utility.

So: establish context, then choose proportionally. Both steps are mandatory, and the first one is the
one that gets skipped.

## 1. Establish context before deciding

Before recommending a structure, adding a project, or generating substantial code, you must know four
things. **Read them out of the repository first** — asking a user a question the code already answers
is the most common way this skill is applied badly.

| Dimension | Read it from | Ask only if |
| -- | -- | -- |
| Complexity (small / medium / large) | Number of functional areas, integrations and external consumers — not file count | The requirements are genuinely not written down anywhere |
| Project type | SDK attribute, `OutputType`, `Microsoft.NET.Sdk.Web`, an AppHost project, a `Main` | The repository is empty |
| Persistence | `PackageReference` to an EF Core provider, `DbContext`, a connection string, a `Purview.Containers` resource | Persistence is being introduced now |
| Telemetry | `Purview.Telemetry.SourceGenerator`, `ILogger` injection, `ActivitySource`, an OTLP exporter | Nothing is configured and you are about to add it |

Identifier generation is a **conditional** dimension: resolve it only when persistence exists *and*
GUID keys are in play. See `purview-persistence`.

Judge complexity on requirements, not size. A 2,000-line CLI with one output format is small. A
900-line service that reconciles three upstream systems and owns money movement is not.

**Do not re-ask.** A dimension answered by repository configuration, existing source, a project file,
an earlier turn in the conversation, or a recorded architectural decision is answered. Ask concise,
targeted questions only for the unknowns that change your recommendation — and if nothing changes,
ask nothing.

Full decision procedure: `references/project-context-discovery.md`.

## 2. Choose proportionally

| Scale | Shape | What you still do not add |
| -- | -- | -- |
| Small | One project, or two when a test project needs a non-trivial subject. Direct implementations. | Repository over `DbContext`, CQRS, a mapping layer, speculative interfaces |
| Medium | Explicit boundaries where complexity already hurts — usually a domain project and a host | Event sourcing, a generic infrastructure framework, a second infrastructure implementation nobody asked for |
| Large | Modularity, independent infrastructure implementations, enforceable boundaries (see the playbook's Architecture-As-Tests page) | Patterns adopted for symmetry rather than for a named problem |

Architecture is allowed to arrive late. A boundary added when the second caller appears is cheap; a
boundary added in anticipation of a second caller that never appears is permanent.

**Never add a project for symmetry.** "Domain, Application, Infrastructure, API" is a response to a
problem, not a starting position. If you cannot name the failure a layer prevents in this codebase,
do not add the layer.

Full guidance and the per-pattern justification tests: `references/proportional-architecture.md`.

## 3. Name what you do create

A project name states a **responsibility**, optionally qualified by an implementation:

```text
<Responsibility>[.<Implementation>]
```

The responsibility decides the rules; the qualifier never changes them. Three rules people usually
get wrong:

- **Do not qualify a single implementation.** `Persistence` is correct even when it uses SQL Server.
Rename when a second provider arrives, not in anticipation.
- **Do not create an empty parent.** `Persistence.SqlServer` + `Persistence.PostgreSql` with nothing
shared need no `Persistence` project.
- **Multiple implementations do not require multiple assemblies.** Two small adapters with the same
dependencies belong in one project.

Never use a *category* as a project name — `Infrastructure`, `Services`, `Managers`, `Models`,
`Helpers`, `Common`. Nothing can be excluded from a category, so it grows without limit. And never
rename an established project just to match canonical terminology; `Domain`, `UseCases` and
`Integrations` are recognised aliases.

Responsibility catalogue, dependency matrix and the naming decision tree:
`references/project-responsibilities.md`.

## 4. Check the direction

`A → B` means **A depends on B at compile time** — not a runtime call, a DI registration, an HTTP
request or an Aspire orchestration edge. Confusing those is the most common source of a wrong answer
about whether a dependency is allowed.

The rules that matter most:

- Core depends on nothing but `Shared`.
- Application never references a concrete `Persistence`, `Providers` or `Adapters` project.
- Composition references implementations; **nothing references Composition** except a host.
- Sibling implementations (`Providers.GitHub`, `Providers.AzureDevOps`) stay independent.
- Persistence owns migration *definitions*; `Migrator` owns their *execution*. Production API startup
does not apply schema migrations.

When a boundary is worth having, make it fail the build rather than the review — see
`purview-architecture-testing`.

## 5. Choose how an API and its clients share a contract

When there is an HTTP API and at least one client, choose **Contracts**, **OpenAPI** or **Hybrid**
from the clients that **exist today**. A planned client is noted; it does not count.

| Clients today | Strategy | Failure it prevents |
| -- | -- | -- |
| All .NET, same solution, shipped in lockstep | **Contracts** — a library of request/response types on the domain's value objects, referenced by the server and every .NET client | Rules drifting, and a weaker second copy of them in the UI |
| At least one non-.NET client, or one that ships independently | **Hybrid** — Contracts is the source; the OpenAPI document and its schemas are generated from it | Non-.NET clients hand-writing shapes, and .NET clients losing their value objects to a generated client |
| No .NET client — partners or the public only | **OpenAPI**, generated from the server's types; no Contracts project | A project with no consumer but the server |

The rules that do not bend:

- **OpenAPI is an output, never an input**, while Contracts exists. Never regenerate a C# client from
your own API's document when that client is in the same solution.
- **Never hand-maintain a parallel set of models**, and **the server always validates** — a shared
validator is for user experience, not trust.
- **Contracts is browser-safe** when a WebAssembly client references it: architecture-policy bans,
the trim and AOT analyzers and the browser platform analyzer check it, not a reviewer.
- **A BFF is earned by keeping tokens out of the browser.** It holds the tokens, gives the browser an
`HttpOnly` cookie, forwards with the user's token, and requires an anti-forgery header on every
forwarded call. In Blazor it is the host that serves the WebAssembly client, and a BFF that only
forwards does not reference Contracts. The policy does not define a BFF yet, so these are review
obligations until a future Build SDK release adds Blazor and BFF support.

Full procedure, version-skew rules, BFF guidance and worked examples: `references/contract-strategy.md`.

## 6. DRY, applied where it pays

Reuse the Purview libraries when they remove a real source of truth, not for coverage:

| Concern | Library | Use it when |
| -- | -- | -- |
| Operation outcomes without exceptions for control flow | `Purview.Results` | Failures are expected and the caller must handle them |
| Primitive obsession, validated identity types | `Purview.ValueObjects` | A value has rules that would otherwise be re-checked at each boundary |
| Schema shared with a TypeScript client | `Purview.ZodSharp` | A contract is duplicated between C# and TypeScript today |
| Local dependencies for dev and integration tests | `Purview.Containers` | Tests need a real database, queue or emulator |
| Logging, tracing and metrics from one declaration | `Purview.Telemetry.SourceGenerator` | Always — see `purview-telemetry` |

Duplication that matters: validation repeated per entry point, a domain rule restated in the UI,
configuration bound in two places, two types that both claim to be the truth. Duplication that does
not: two similar-looking methods that answer to different owners.

## 7. Telemetry is not an advanced topic

Recommend instrumentation from the first commit, at every project size. A small application gets
small telemetry — it does not get none. See `purview-telemetry`.

## Checks before you answer

- Did you read the four context dimensions rather than ask for them?
- Can you name, for each project and each abstraction you are proposing, the failure it prevents here?
- Is anything in your proposal there only because a reference architecture has it?
- If there is an API with clients, did you choose its contract strategy from the clients that exist
today?
- Did you include telemetry?

## See also

- `purview-telemetry` — instrumentation at every scale
- `purview-persistence` — storage and GUID-key guidance, only when persistence is real
- [Engineering Principles](https://github.com/purview-dev/engineering-playbook/blob/main/docs/wiki/Engineering-Principles.md)
- [Adopting This At Your Scale](https://github.com/purview-dev/engineering-playbook/blob/main/docs/wiki/Adopting-This-At-Your-Scale.md)
Loading
Loading