diff --git a/Compono.slnx b/Compono.slnx
index e023dd0..5ec5559 100644
--- a/Compono.slnx
+++ b/Compono.slnx
@@ -3,6 +3,7 @@
+
diff --git a/docs/mvp.md b/docs/mvp.md
index 3cb86d4..86b4511 100644
--- a/docs/mvp.md
+++ b/docs/mvp.md
@@ -270,10 +270,12 @@ package: substitutable-shape rules including delegate types, `NSubstituteOptions
diagnostics). **ADR-0024's core engine extension point is implemented
(PLAN-0005 Phase 0)** — `builder.AddSemanticProvider(...)`/
`builder.AddTestDoubleProvider(...)` are real, tested public API today.
-**`Compono.NSubstitute` itself (ADR-0025) is not yet implemented** — see
+**`Compono.NSubstitute` itself (ADR-0025) is implemented (PLAN-0005 Phase 1)**
+— `NSubstituteProvider`/`NSubstituteOptions`/`UseNSubstitute()` are real code,
+but not yet test-covered or end-to-end verified (PLAN-0005 Phase 2) — see
[PLAN-0005](plans/0005-milestone-5-nsubstitute-integration.md) for the
phase-by-phase tracker; this milestone's own exit criteria aren't met until
-that package ships.
+that verification lands.
### Scope
diff --git a/docs/plans/0005-milestone-5-nsubstitute-integration.md b/docs/plans/0005-milestone-5-nsubstitute-integration.md
index 3a12435..16784e0 100644
--- a/docs/plans/0005-milestone-5-nsubstitute-integration.md
+++ b/docs/plans/0005-milestone-5-nsubstitute-integration.md
@@ -123,15 +123,15 @@ Each phase ships as its own PR, per `design-decisions.md`'s phase rule.
### Phase 1: `Compono.NSubstitute` package (ADR-0025)
-**Status:** Not Started
+**Status:** Done
-- [ ] New `src/Compono.NSubstitute/Compono.NSubstitute.csproj` (matching
+- [x] New `src/Compono.NSubstitute/Compono.NSubstitute.csproj` (matching
`Compono.XunitV3.csproj`'s TFM/packaging shape — `PackageReference` to
`Compono` + `NSubstitute`, `PrivateAssets="none"` on the `Compono`
reference per PLAN-0004 Phase 3's real packaging-bug lesson).
-- [ ] `NSubstituteOptions`: `SubstituteAbstractClasses` (`bool`, default
+- [x] `NSubstituteOptions`: `SubstituteAbstractClasses` (`bool`, default
`true`).
-- [ ] `NSubstituteProvider : ICompositionValueProvider`: the
+- [x] `NSubstituteProvider : ICompositionValueProvider`: the
`IsSubstitutable` static check (interface / delegate / conditionally
unsealed-abstract-class), `Substitute.For(Type[], object[])` call for
a match, `NotHandled` otherwise, no `context.Resolve()` call
@@ -146,7 +146,7 @@ Each phase ships as its own PR, per `design-decisions.md`'s phase rule.
**not** `typeof(Delegate).IsAssignableFrom(requestedType)` — the
latter wrongly matches the non-substitutable `Delegate`/
`MulticastDelegate` base types themselves.
-- [ ] `CompositionBuilderExtensions.UseNSubstitute()`/
+- [x] `CompositionBuilderExtensions.UseNSubstitute()`/
`UseNSubstitute(Action)` extension methods
(C# 14 extension-block syntax, per `coding-standards.md`).
@@ -220,12 +220,22 @@ Each phase ships as its own PR, per `design-decisions.md`'s phase rule.
versus `Compono.NSubstitute` itself not yet — done early, same PR
#28 review round. Delegate substitution already in the scope list
from the design-phase draft.
-- [ ] `docs/mvp.md`: mark this milestone's exit criteria met once
- `Compono.NSubstitute` actually ships (Phase 1/2) — still open.
+- [x] `docs/mvp.md`: updated again in the Phase 1 PR (Codex, P2, same
+ pattern as the PR #28 round above) — `Compono.NSubstitute` (ADR-0025)
+ now says "implemented (PLAN-0005 Phase 1)," with test
+ coverage/end-to-end verification called out as still pending
+ (Phase 2), rather than leaving the whole package described as "not
+ yet implemented" once the Phase 1 PR actually shipped it. Milestone
+ exit criteria still correctly not marked met — that still needs
+ Phase 2.
- [x] `docs/public-api.md`: Provider Extensibility section now says
"Implemented" rather than "design target" — done early, same PR #28
review round. NSubstitute Integration section correctly still says
"not yet implemented."
+- [x] `docs/public-api.md`: NSubstitute Integration section updated again
+ in the Phase 1 PR (Codex, P2) — now says "Implemented (PLAN-0005
+ Phase 1)" rather than "not yet implemented," same reasoning as the
+ `docs/mvp.md` update above.
- [x] `docs/public-api.md`: Naming Vocabulary gains `ICompositionValueProvider`
— already added during the design phase.
- [x] `docs/adr/README.md`/`docs/plans/README.md` index rows (already added
@@ -317,6 +327,39 @@ alpha must:
## Notes
+**Phase 1 (Done):**
+
+- Implemented exactly per ADR-0025's Amendment 1 corrected shapes — no
+ deviation from the ADR's own code sketches for `NSubstituteOptions`,
+ `NSubstituteProvider`, or `CompositionBuilderExtensions`.
+- `Compono.NSubstitute.csproj` mirrors `Compono.XunitV3.csproj`'s shape:
+ `net10.0;net11.0` TFMs, `ProjectReference` to `Compono` with
+ `PrivateAssets="none"` (PLAN-0004 Phase 3's packaging lesson, applied
+ proactively rather than rediscovered), `PackageReference` to
+ `NSubstitute` (version centrally managed, already present in
+ `Directory.Packages.props` from the test-project usage), and
+ `InternalsVisibleTo` for the not-yet-created `Compono.NSubstitute.Tests`
+ (Phase 2) so `IsSubstitutable` can stay `internal`.
+ `docs/architecture.md`'s package-dependency diagram already lists
+ `Compono.NSubstitute → Compono, NSubstitute` — matched exactly, no
+ reference to any other package.
+- Added to `Compono.slnx`. Whole-solution `dotnet build` green, no warnings.
+- No test project yet (Phase 2) — `IsSubstitutable`/`NSubstituteProvider`/
+ `UseNSubstitute` are implemented but only build-verified in this phase,
+ not test-verified; that's Phase 2's job per the plan's own phase split.
+- **PR review (Codex, one P2 finding) caught the same doc-staleness pattern
+ PR #28's second review round caught for Phase 0.** `docs/public-api.md`'s
+ NSubstitute Integration section and `docs/mvp.md`'s Milestone 5 section
+ both still said `Compono.NSubstitute` was "not yet implemented" after
+ this phase's package/provider/`UseNSubstitute()` code had already landed
+ in the same PR — pulled forward and fixed in this PR rather than
+ deferred to Phase 3, on the same reasoning as the earlier round: leaving
+ the docs contradict the code until a later phase lands is worse than a
+ small out-of-phase doc correction. Both now say "implemented (PLAN-0005
+ Phase 1)" and explicitly call out that test coverage/end-to-end
+ verification remain pending (Phase 2) — this milestone's exit criteria
+ still correctly not marked met.
+
**Phase 0 (Done):**
- Implemented in the same branch/PR as the design docs, per explicit user
diff --git a/docs/public-api.md b/docs/public-api.md
index 18e66c7..0a336be 100644
--- a/docs/public-api.md
+++ b/docs/public-api.md
@@ -422,9 +422,11 @@ same interface, registered via `AddSemanticProvider` instead of
## NSubstitute Integration
-Design target, resolved (not yet implemented) by
+Implemented (PLAN-0005 Phase 1) per
[ADR-0025](adr/0025-compono-nsubstitute-package-design.md), built on the Provider
-Extensibility contract above.
+Extensibility contract above — `Compono.NSubstitute`'s `NSubstituteProvider`/
+`NSubstituteOptions`/`UseNSubstitute()` are real code today. Test coverage and
+end-to-end verification are still pending (PLAN-0005 Phase 2).
Activation:
diff --git a/src/Compono.NSubstitute/Compono.NSubstitute.csproj b/src/Compono.NSubstitute/Compono.NSubstitute.csproj
new file mode 100644
index 0000000..f11047d
--- /dev/null
+++ b/src/Compono.NSubstitute/Compono.NSubstitute.csproj
@@ -0,0 +1,23 @@
+
+
+
+ latest
+ enable
+ enable
+ net10.0;net11.0
+ NSubstitute integration for Compono - a test-double provider composing interface, delegate, and (optionally) abstract-class parameters as real NSubstitute substitutes. See docs/adr/0024-public-provider-extensibility-model.md and docs/adr/0025-compono-nsubstitute-package-design.md.
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Compono.NSubstitute/CompositionBuilderExtensions.cs b/src/Compono.NSubstitute/CompositionBuilderExtensions.cs
new file mode 100644
index 0000000..2b7c566
--- /dev/null
+++ b/src/Compono.NSubstitute/CompositionBuilderExtensions.cs
@@ -0,0 +1,28 @@
+namespace Compono;
+
+///
+/// Activates on a . See
+/// docs/adr/0025-compono-nsubstitute-package-design.md.
+///
+public static class CompositionBuilderExtensions
+{
+ extension(CompositionBuilder builder)
+ {
+ ///
+ /// Registers an with default .
+ ///
+ public CompositionBuilder UseNSubstitute() => builder.UseNSubstitute(static _ => { });
+
+ ///
+ /// Registers an , configured by .
+ ///
+ /// Sets the provider's .
+ public CompositionBuilder UseNSubstitute(Action configure)
+ {
+ ArgumentNullException.ThrowIfNull(configure);
+ var options = new NSubstituteOptions();
+ configure(options);
+ return builder.AddTestDoubleProvider(new NSubstituteProvider(options));
+ }
+ }
+}
diff --git a/src/Compono.NSubstitute/NSubstituteOptions.cs b/src/Compono.NSubstitute/NSubstituteOptions.cs
new file mode 100644
index 0000000..11cde0d
--- /dev/null
+++ b/src/Compono.NSubstitute/NSubstituteOptions.cs
@@ -0,0 +1,15 @@
+namespace Compono;
+
+///
+/// Configuration for , set via
+/// CompositionBuilderExtensions.UseNSubstitute(Action{NSubstituteOptions}). See
+/// docs/adr/0025-compono-nsubstitute-package-design.md.
+///
+public sealed class NSubstituteOptions
+{
+ ///
+ /// Whether an unsealed abstract class is substitutable, in addition to every interface and
+ /// delegate type. Defaults to .
+ ///
+ public bool SubstituteAbstractClasses { get; set; } = true;
+}
diff --git a/src/Compono.NSubstitute/NSubstituteProvider.cs b/src/Compono.NSubstitute/NSubstituteProvider.cs
new file mode 100644
index 0000000..eaf4c63
--- /dev/null
+++ b/src/Compono.NSubstitute/NSubstituteProvider.cs
@@ -0,0 +1,46 @@
+using NSubstitute;
+
+namespace Compono;
+
+///
+/// A stage-6 test-double provider that composes an interface, delegate, or (when
+/// allows it) unsealed abstract-class
+/// request as a real NSubstitute substitute, via .
+/// Registered via CompositionBuilderExtensions.UseNSubstitute(). See
+/// docs/adr/0025-compono-nsubstitute-package-design.md (Amendment 1 for the corrected shape
+/// below).
+///
+public sealed class NSubstituteProvider : ICompositionValueProvider
+{
+ private readonly bool _substituteAbstractClasses;
+
+ ///
+ /// Creates an , snapshotting 's
+ /// current values - the provider never retains the caller-owned
+ /// instance itself, so a caller mutating it after construction can't change this
+ /// already-registered provider's behavior (ADR-0024's immutable-provider-registration guarantee).
+ ///
+ /// The options to snapshot.
+ public NSubstituteProvider(NSubstituteOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(options);
+ _substituteAbstractClasses = options.SubstituteAbstractClasses;
+ }
+
+ ///
+ public CompositionProviderResult TryProvide(in CompositionProviderRequest request, ICompositionContext context)
+ {
+ if (!IsSubstitutable(request.RequestedType, _substituteAbstractClasses))
+ return CompositionProviderResult.NotHandled;
+
+ return CompositionProviderResult.Handled(Substitute.For([request.RequestedType], []));
+ }
+
+ // ADR-0025 Amendment 1: IsSubclassOf(typeof(MulticastDelegate)), not
+ // typeof(Delegate).IsAssignableFrom(requestedType) - the latter wrongly matches the
+ // non-substitutable Delegate/MulticastDelegate framework base types themselves.
+ internal static bool IsSubstitutable(Type requestedType, bool substituteAbstractClasses) =>
+ requestedType.IsInterface
+ || requestedType.IsSubclassOf(typeof(MulticastDelegate))
+ || (substituteAbstractClasses && requestedType.IsAbstract && !requestedType.IsSealed && !requestedType.IsInterface);
+}
diff --git a/src/Compono.XunitV3/Compono.XunitV3.csproj b/src/Compono.XunitV3/Compono.XunitV3.csproj
index 699e03b..3fe7b91 100644
--- a/src/Compono.XunitV3/Compono.XunitV3.csproj
+++ b/src/Compono.XunitV3/Compono.XunitV3.csproj
@@ -9,17 +9,10 @@
-
+