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
33 changes: 33 additions & 0 deletions .config/architecture-policy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"$schema": "https://raw.githubusercontent.com/purview-dev/architests/main/architecture-policy.schema.json",
"version": 3,
"description": "Purview.Aspire.ResourceKit composes an Aspire AppHost from strongly typed kits at runtime, and generates the wiring that makes that possible at build time. The direction that matters is build-time-tooling-may-not-know-the-runtime: the Roslyn components are loaded into a consumer's compiler, so a reference to the runtime from one of them would drag Aspire.Hosting into every consumer's compiler host.",
"sourceRoots": ["src"],
"responsibilities": [
{
"name": "ResourceKit",
"category": "composition",
"purpose": "The runtime model an AppHost composes: host and resource kit base types, generated options, run modes and OptionsHelper. This is the composition root's own library, and the only project packed for consumers.",
"mayDependOn": ["SourceGeneration"],
"mustNotDependOn": ["Example"],
"forbiddenPackagePrefixes": ["Microsoft.CodeAnalysis"],
"mayRemainCombinedWhen": "Its reference to SourceGeneration is analyzer-only (ReferenceOutputAssembly=false); the generator and its code fixes are what make the kit classes strongly typed, and they must never appear as an assembly dependency."
},
{
"name": "SourceGeneration",
"category": "infrastructure",
"purpose": "Build-time tooling: the incremental generator that emits kit bases and options, the diagnostics and suppressor, the OptionsHelper code fix, and the attribute sources. Loaded into a consumer's compiler, never into a running application.",
"mayDependOn": [],
"mustNotDependOn": ["ResourceKit", "Example"],
"forbiddenPackagePrefixes": ["Aspire.Hosting", "Microsoft.Extensions.Options"]
},
{
"name": "Example",
"category": "host",
"purpose": "Sample Aspire applications that consume the toolkit the way a real AppHost does - one composed through the generated pattern, one wired by hand - plus the service and service-defaults projects they start.",
"mayDependOn": ["ResourceKit", "SourceGeneration"],
"mustNotDependOn": []
}
],
"waivers": []
}
4 changes: 2 additions & 2 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@
"rollForward": false
},
"dotnet-inspect": {
"version": "0.26.0",
"version": "0.27.0",
"commands": [
"dotnet-inspect"
],
"rollForward": false
},
"purview.build": {
"version": "0.7.1",
"version": "0.8.1",
"commands": [
"purview-build"
],
Expand Down
1 change: 1 addition & 0 deletions .config/purview-build.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"RequiredContent": {
"purview.aspire.resourcekit": [
"analyzers/dotnet/cs/Purview.Aspire.ResourceKit.SourceGeneration.dll",
"analyzers/dotnet/cs/Purview.Aspire.ResourceKit.SourceGeneration.CodeFixes.dll",
"buildTransitive/Purview.Aspire.ResourceKit.props",
"lib/$(TFM)/Purview.Aspire.ResourceKit.dll",
"lib/$(TFM)/Purview.Aspire.ResourceKit.xml",
Expand Down
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Purview.BuildSdk 1.1.0 - managed file, do not edit
# Purview.BuildSdk 1.3.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
Expand Down
6 changes: 5 additions & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,9 @@ Keep product, architecture, and general engineering standards centralized in `AG

- Apply the `AGENTS.md` testing bar strictly (TUnit/TUnit.Mocks, AAA comments, naming, cancellation token rule).
- Treat work as incomplete until relevant tests pass.
- Consult the repository `.agents/` folder for additional skills/workflows that may improve execution quality.
- Product, architecture and architecture-policy rules are enforced by tests; read them before changing a
project reference (`src/tests/Repository.UnitTests/ArchitectureTests.cs`).
- Skills this repository authors live in `skills/<name>/SKILL.md`. `.agents/skills/` and `.claude/skills/`
are the skills CLI's install locations and are gitignored, so a fresh clone has none — do not read
guidance from there or write to it.
- Keep edits minimal, focused, and aligned with existing SDK and repository conventions.
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,15 @@ sketch

# End of https://www.toptal.com/developers/gitignore/api/openframeworks+visualstudio,visualstudiocode,jetbrains,react,node,astro,aspnetcore,ncrunch,go

# Agent skills installed by the skills CLI.
#
# The skills CLI is the single installer for agent skills (Purview.BuildSdk 1.0.6 removed the
# package-side mirror), and it writes into every agent's own directory. What a developer installed is
# their choice, not a repository artefact, so these directories are ignored and `skills-lock.json` -
# the record of what this repository pins - is committed instead.
.agents/
.claude/

!scripts/*
!src/src/ResourceKit/build/
!src/src/ResourceKit/build/**
Expand Down
22 changes: 18 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,19 @@ At a high level:

This is a .NET repository centered on Aspire and source generation.

- **.NET 10 is the floor and the only leg**: `src/Versions.props` declares `DefaultTargetFramework`,
every project inherits it, and the package ships a single `lib/net10.0` asset. Do not add a
multi-targeting leg without giving it its own lowest-compatible `Microsoft.Extensions.*` floor.
- The two Roslyn components (`SourceGeneration`, `SourceGeneration.CodeFixes`) target `netstandard2.0`
by SDK rule — a generator runs inside the compiler and IDE host, not on the consumer's framework.
- Uses the **Purview DotNet Project SDK** (`Purview.BuildSdk`) for project defaults and conventions.
- Uses centrally-managed package versions (`Directory.Packages.props`).
- Uses **TUnit** and **TUnit.Mocks** for testing.
- Uses `Microsoft.Testing.Platform` via package-managed tooling.

For additional agent capabilities, conventions, and reusable workflows, use the repository’s `.agents/` folder.
Do not assume this file contains every available helper; consult `.agents/` as part of planning and execution.
The skills this repository **authors** live in the root `skills/` folder — read them there, at
`skills/<name>/SKILL.md`. Skills a *package* ships are installed into `.agents/skills/` by the skills
CLI (which is gitignored here, so a fresh clone has none until you run `npx skills add`).

## Purview DotNet Project SDK guidance

Expand Down Expand Up @@ -56,6 +62,9 @@ Use naming that mirrors production structure:

- Test namespace should match the target namespace shape (same hierarchy intent).
- Test class naming: `{ClassUnderTest}Tests`
- A suite whose subject is a contract spanning several types is named for its scope instead — for
example `ResourceKitLifecycleTests` covers the `Build`/`Configure` contract of both `HostKitBase` and
`ResourceKitBase`. Forcing a `{ClassUnderTest}Tests` name onto one of those is less truthful, not more.
- Test method naming: `{SubjectOrMethodUnderTest}_{Scenario}_{Expectation}`

### Cancellation token rule
Expand All @@ -71,7 +80,12 @@ Before considering a change done:

1. Relevant tests are updated/added using the conventions above.
2. Test runs pass for the impacted scope.
3. New/updated docs stay consistent with this file and the `.agents/` guidance.
3. New/updated docs stay consistent with this file and the authored skills in `skills/`.
4. A change to a project reference is reflected in `.config/architecture-policy.json` — the policy is
enforced by `src/tests/Repository.UnitTests/ArchitectureTests.cs`, and widening `mayDependOn` is a
reviewed policy change rather than a convenient edit.
5. A change to what the package ships, or to a claim the README/diagnostics pages make about it, is
verified with `just verify-consumers` — it packs the package and builds a consumer that installs it.

## Agent skills

Expand All @@ -92,7 +106,7 @@ npx skills add purview-dev/aspire-resourcekit
| Skill | Answers |
| --- | --- |
| `skills/purview-resourcekit-authoring/` | declaring a `[HostKit]` and resource kits, attribute styles, the Build/Configure lifecycle, project resources |
| `skills/purview-resourcekit-options/` | generated options, extending them, `IsEnabled` versus `IsResourceEnabled`, `OptionsHelper` |
| `skills/purview-resourcekit-options/` | generated options, extending them, run modes (`Mode`), `IsEnabled` versus `IsResourceEnabled`, `OptionsHelper` |
| `skills/purview-resourcekit-testing/` | TUnit.Aspire fixtures, overriding options from a test, running the tiers |
| `skills/purview-resourcekit-diagnostics/` | what an `SG00xx` means and how to fix it |
| `skills/purview-resourcekit-migration/` | converting an existing inline AppHost into ResourceKit composition |
Expand Down
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,38 @@ All notable changes to `Purview.Aspire.ResourceKit` are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- **Run modes** — `ResourceKitMode` (`All`, `LocalDevelopment`, `Run`, `Publish`) selects which AppHost
execution a host kit or resource kit participates in. Declare the default on `[HostKit]` /
`[ResourceDefinition]` (named property or constructor argument), override it from configuration
(`<HostKit>:Mode`, `<HostKit>:<Resource>:Mode`), and `Build` gates on it before `IsEnabled` and
`IsResourceEnabled(builder)`, so publish-only and local-development-only resources need no code.

### Changed

- **Breaking: .NET 10 is now the minimum.** The runtime targets `net10.0` only — the `net8.0` and
`net9.0` legs are removed, so a consuming AppHost must be on .NET 10. This also drops the
`Microsoft.Extensions.*` 10.x floor those legs previously forced onto `net8.0` consumers, which was
the whole point: a package should not vendor a framework's abstractions to consumers on an older
framework. Raising a framework floor requires a **major** version.
- **Repository guardrails** — `.config/architecture-policy.json` plus
`src/tests/Repository.UnitTests/ArchitectureTests.cs` now enforce the dependency direction between the
runtime, the Roslyn components and the samples, and `just verify-consumers` packs the package and builds
a real consumer against it (one that installs it by `PackageReference` and is generated into, which no
test in this repository previously proved).
- **Agent skill delivery documented correctly** — `README.md` no longer claims the package installs skills
into a consuming repository on build. That mirror was removed in `Purview.BuildSdk` 1.0.6; the skills CLI
is the only installer.

### Removed

- **mkdocs site configuration** — `mkdocs.yml` and the mkdocs-only navigation pages (`index.md`,
`Home.md`, `_Sidebar.md`) are removed. The `docs/wiki` topic pages remain the source consumed by
<https://purview.dev/docs/aspire-resourcekit/>.

## [1.0.0] - 2026-09-29

The first stable release of `Purview.Aspire.ResourceKit`.
Expand Down
9 changes: 5 additions & 4 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<RoslynVersion>5.9.0</RoslynVersion>
<TUnitVersion>1.76.0</TUnitVersion>
<PurviewSourceGeneratorFrameworkVersion>1.2.1</PurviewSourceGeneratorFrameworkVersion>
<PurviewTelemetryVersion>5.0.6</PurviewTelemetryVersion>
<PurviewSourceGeneratorFrameworkVersion>1.3.0</PurviewSourceGeneratorFrameworkVersion>
<PurviewTelemetryVersion>5.2.0</PurviewTelemetryVersion>
<DotNetRuntimeVersion>10.0.13</DotNetRuntimeVersion>
<MSExtensionVersion>10.11.0</MSExtensionVersion>
<OTelVersion>1.20.1</OTelVersion>
Expand Down Expand Up @@ -34,7 +34,7 @@
Include="Microsoft.Extensions.Options.ConfigurationExtensions"
Version="$(DotNetRuntimeVersion)"
/>
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.402" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.403" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="$(DotNetRuntimeVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="$(RoslynVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="$(RoslynVersion)" />
Expand All @@ -52,7 +52,8 @@
<PackageVersion Include="TUnit.Core" Version="$(TUnitVersion)" />
<PackageVersion Include="TUnit.Aspire" Version="$(TUnitVersion)" />
<PackageVersion Include="TUnit.Mocks" Version="$(TUnitVersion)" />
<PackageVersion Include="Purview.Skills.Testing" Version="1.1.0" />
<PackageVersion Include="Purview.Skills.Testing" Version="1.2.0" />
<PackageVersion Include="Purview.Architecture.Testing" Version="1.2.0" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="$(MSExtensionVersion)" />
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="$(MSExtensionVersion)" />
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="$(OTelVersion)" />
Expand Down
8 changes: 8 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,14 @@ pipeline-pack-validate *args:
echo "Running pack + validate pipeline..."
{{ pipeline_tool }} --Build:RunPack=true --Build:ValidatePack=true --Release:Mode=None {{ args }}

# Verify the package against a real consumer: pack, assert the package shape, then build a
# throwaway consumer that installs it. Restores from nuget.org, so it is not part of the unit tier.
[group('Pipeline')]
verify-consumers *args:
just ensure-pipeline-tool
echo "Verifying packaged consumers..."
dotnet msbuild scripts/verify-consumers.proj {{ args }}

# Open the solution in Visual Studio/ Registered application
[group('Utilities')]
vs:
Expand Down
39 changes: 21 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@

If your AppHost is getting bigger, this helps you keep resource setup maintainable and discoverable by moving composition into focused resource classes and generating the plumbing for you.

> [!NOTE]
> `Purview.Aspire.ResourceKit` 1.0.0 is the first stable release. See the [changelog](CHANGELOG.md) and [release notes](docs/wiki/Release-Notes.md).
> [!IMPORTANT]
> Requires **.NET 10**. The package targets `net10.0` only — there is no `net8.0` or `net9.0` asset.
> See [Versioning and compatibility](docs/wiki/Versioning.md#supported-target-frameworks).
>
> For what changed, see the [changelog](CHANGELOG.md) and [release notes](docs/wiki/Release-Notes.md).

> [!TIP]
> **Lifecycle quick links**
>
> - Build vs Configure guidance: [`docs/wiki/Lifecycle-Build-Configure.md`](docs/wiki/Lifecycle-Build-Configure.md)
> - Runtime enablement (`IsEnabled` + `IsResourceEnabled(...)`): [`docs/wiki/Enablement.md`](docs/wiki/Enablement.md)
> - Runtime enablement and run modes (`Mode` + `IsEnabled` + `IsResourceEnabled(...)`): [`docs/wiki/Enablement.md`](docs/wiki/Enablement.md)

## Why teams use it

Expand Down Expand Up @@ -48,22 +51,21 @@ builder.AddAspireResourceKit();
The extension method name is generated from your host metadata; in this repository sample it is `AddAspireResourceKit()`.

> [!TIP]
> **Built-in AI agent skills (auto-installed via NuGet)**
>
> This package can ship one or more bundled [Agent Skills](https://agentskills.io/).
> When a consuming project builds, any `skills/**/SKILL.md` files in the package are copied to:
>
> - `.agents/skills/**`
>
> Example:
> **Agent skills for your coding agent**
>
> - `skills/aspire-apphost-to-resourcekit/SKILL.md` → `.agents/skills/aspire-apphost-to-resourcekit/SKILL.md`
> This repository authors [Agent Skills](https://agentskills.io/) describing ResourceKit — declaring a
> `[HostKit]` and resource kits, the generated options and run modes, test fixtures, and what each
> `SG00xx` diagnostic means.
>
> It also writes a local `.gitignore` into each generated skill folder to keep updates out of source control noise.
> They are **not** shipped in the NuGet package, and nothing copies them into your repository on build.
> The skills CLI clones the git repository and installs them into your agent's own skill directory:
>
> To disable this behavior, set the shared opt-out property in your project (or `Directory.Build.props`):
> ```bash
> npx skills add purview-dev/aspire-resourcekit
> ```
>
> `<EnableAgentFolderInPackage>false</EnableAgentFolderInPackage>`
> Append `#<ref>` to pin a commit or tag. The CLI records the installed set in `skills-lock.json`, so it
> can be restored later with `npx skills experimental_install`.

## Lifecycle mental model

Expand All @@ -74,11 +76,12 @@ ResourceKit has two layers of lifecycle methods:

Enablement is evaluated *before* resource construction:

- `IsEnabled` is the persisted toggle (typically from generated options).
- `Mode` selects which AppHost execution a kit participates in (`All`, `LocalDevelopment`, `Run`, `Publish`). It is configured through the generated options and can be declared on `[HostKit]`/`[ResourceDefinition]`.
- `IsEnabled` is the persisted toggle (typically from generated options) and the single member consumers need to check.
- `IsResourceEnabled(builder)` is the runtime hook used by `Build` to decide whether this resource should run now.
- If it evaluates to `false`, both `BuildResource(...)` and `ConfigureResource()` are skipped for that resource.
- If any gate excludes the resource, both `BuildResource(...)` and `ConfigureResource()` are skipped for that resource.

Use `IsResourceEnabled(builder)` when enablement depends on runtime state (environment, config, publish mode, etc.), not just static options.
Use `Mode` for publish-only or local-development-only resources, and `IsResourceEnabled(builder)` when enablement depends on runtime state beyond an execution mode (environment probes, dynamic configuration).

## What gets generated

Expand Down
2 changes: 1 addition & 1 deletion docs/wiki/Agent-Skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ records what it installed in `skills-lock.json`, so the set can be restored late
| Skill | Answers |
| --- | --- |
| `purview-resourcekit-authoring` | declaring a `[HostKit]` and resource kits, attribute styles, the Build/Configure lifecycle, project resources |
| `purview-resourcekit-options` | generated options, extending them, `IsEnabled` versus `IsResourceEnabled`, `OptionsHelper` |
| `purview-resourcekit-options` | generated options, extending them, run modes (`Mode`), `IsEnabled` versus `IsResourceEnabled`, `OptionsHelper` |
| `purview-resourcekit-testing` | TUnit.Aspire fixtures, overriding options from a test, running the tiers |
| `purview-resourcekit-diagnostics` | what an `SG00xx` means and how to fix it |
| `purview-resourcekit-migration` | converting an existing inline AppHost into ResourceKit composition |
Expand Down
Loading
Loading