[PLAN-0005] Milestone 5: NSubstitute Integration¶
Status: In Progress
Implements: ADR-0024 (core public provider extensibility: ICompositionValueProvider, CompositionProviderRequest/CompositionProviderResult, AddSemanticProvider/AddTestDoubleProvider, the PublicProviderAdapter/ ProviderType diagnostics-identity mechanism), ADR-0025 (Compono.NSubstitute package: NSubstituteProvider, NSubstituteOptions, UseNSubstitute, substitutable-shape rules, diagnostics)
Note: both ADRs are Accepted as of the design review that produced this plan (2026-07-31) — implementation may begin.
Goal¶
var composer = Composer.Create(builder => builder.UseNSubstitute());
[Theory]
[Compose]
public async Task Saves_order(
[Shared] IOrderRepository repository,
CreateOrderHandler handler,
CreateOrder command)
{
await handler.Handle(command);
await repository.Received(1).SaveAsync(Arg.Any<Order>(), Arg.Any<CancellationToken>());
}
runs end-to-end: repository is a real NSubstitute substitute, reused as the exact same instance inside handler's own composed IOrderRepository constructor parameter; a request for an unsubstitutable type (a sealed concrete class) still produces the engine's existing clear diagnostic naming the type and path; no manual Substitute.For<T>() call appears anywhere in the test.
Scope¶
Per ADR-0024/ADR-0025's Decision Outcomes:
- New core
Componosurface:ICompositionValueProvider,CompositionProviderRequest,CompositionProviderResult,CompositionBuilder.AddSemanticProvider/AddTestDoubleProvider, the internalPublicProviderAdapter,ICompositionProvider.ProviderType(default-interface member), and threadingCompositionConfiguration .SemanticProviders/TestDoubleProvidersthroughComposer.Create<T>/CreateMany<T>/CreateRowintoCompositionContext's already-existing (always-empty-until-now)_semanticProviders/_testDoubleProvidersfields. - New
Compono.NSubstitutepackage:NSubstituteProvider,NSubstituteOptions,CompositionBuilderExtensions.UseNSubstitute. - New test project:
test/Compono.NSubstitute.Tests. - Doc updates:
docs/mvp.md,docs/architecture.md,docs/public-api.md.
Explicitly deferred/non-goals — see ADR-0024/ADR-0025's own Deferred Decisions/Non-goals:
- Recursive auto-configuration of substitute members.
- NSubstitute API wrappers (
Arg/Received/Returnsstay NSubstitute's own surface). - A delegate-based convenience overload for lightweight ad hoc custom providers (ADR-0024's contract-shape Pros/Cons notes this as a possible future addition, not designed here).
- Wrapping a provider's own thrown exception into a diagnostic-carrying
CompositionException(ADR-0024's Provider Failure Semantics — an accepted asymmetry, not solved here). Compono.Bogus/Milestone 6 itself — ADR-0024 validates the contract against a Bogus sketch but does not build it.- A
Compono.Benchmarksentry for a substitute-composed graph (nice-to-have, tracked as an open item below, not required for this plan's exit criteria).
Phases¶
Each phase ships as its own PR, per design-decisions.md's phase rule.
Phase 0: Core engine extension point (ADR-0024)¶
Status: Done
-
CompositionProviderRequest(public readonly struct):RequestedType/DeclaringType/Name/Nullability. -
CompositionProviderResult(public readonly struct):NotHandledstatic property,Handled(object? value)static factory, internalIsHandled/Value. -
ICompositionValueProvider(public interface):TryProvide(in CompositionProviderRequest, ICompositionContext). -
ICompositionProvider.ProviderTypedefault-interface member (=> GetType());PublicProviderAdapter : ICompositionProvideroverriding it to report the wrapped provider's real type. -
CompositionContext.TryProviders/InvokeFactory'sproviderparameter passthrough updated to readcandidate.ProviderTypeinstead ofcandidate.GetType(). -
CompositionBuilder.AddSemanticProvider(ICompositionValueProvider)/AddTestDoubleProvider(ICompositionValueProvider): accumulate into ordered lists, same shape as existing rule-builder accumulators. -
CompositionBuilder.Build(): wrap each accumulated provider (in registration order) into aPublicProviderAdapter, assign to two newCompositionConfigurationfields (SemanticProviders,TestDoubleProviders, bothIReadOnlyList<ICompositionProvider>, defaulting to[]). -
Composer.Create<T>/CreateMany<T>/ComposeMany<T>/CreateRow: thread_configuration.SemanticProviders/TestDoubleProvidersthrough toCompositionContext's existing constructor parameters of the same name (currently always passed[]) — noCompositionContextsignature change, only what's passed in at each of these four call sites. -
Compono.Testscoverage for this API in isolation, before anyCompono.NSubstitutecode exists (testing.md's "verifying a new public entry point" rule): a hand-writtenICompositionValueProvidertest double registered via each ofAddSemanticProvider/AddTestDoubleProvider, proving stage placement (5 vs. 6), ordering (registration order, first match wins),NotHandledfalling through to stage ⅞,ProviderAttempt.Providernaming the real provider type through the adapter (notPublicProviderAdapteritself), a[Shared]-equivalent request's successful public-provider result reaching scope and being reused by a later request, and a thrown exception from a public provider propagating uncaught (per ADR-0024's Provider Failure Semantics).
Phase 1: Compono.NSubstitute package (ADR-0025)¶
Status: Done
- New
src/Compono.NSubstitute/Compono.NSubstitute.csproj(matchingCompono.XunitV3.csproj's TFM/packaging shape —PackageReferencetoCompono+NSubstitute,PrivateAssets="none"on theComponoreference per PLAN-0004 Phase 3's real packaging-bug lesson). -
NSubstituteOptions:SubstituteAbstractClasses(bool, defaulttrue). -
NSubstituteProvider : ICompositionValueProvider: theIsSubstitutablestatic check (interface / delegate / conditionally unsealed-abstract-class),Substitute.For(Type[], object[])call for a match,NotHandledotherwise, nocontext.Resolve<T>()call anywhere in the type. Per ADR-0025 Amendment 1: the constructor snapshotsoptions.SubstituteAbstractClassesinto aprivate readonly boolfield — it must never store theNSubstituteOptionsreference itself, so a caller mutating that instance after registration can't change an already-registered provider's behavior (the guarantee ADR-0024 commits to: immutable, reusable provider registration). Delegate matching usesrequestedType.IsSubclassOf(typeof(MulticastDelegate)), nottypeof(Delegate).IsAssignableFrom(requestedType)— the latter wrongly matches the non-substitutableDelegate/MulticastDelegatebase types themselves. -
CompositionBuilderExtensions.UseNSubstitute()/UseNSubstitute(Action<NSubstituteOptions>)extension methods (C# 14 extension-block syntax, percoding-standards.md).
Phase 2: Test suites and verification¶
Status: Done
-
test/Compono.NSubstitute.Tests:IsSubstitutableunit coverage (interface / delegate / unsealed abstract class with the option on / unsealed abstract class with the option off / sealed class / struct /string);NSubstituteProvider.TryProvidereturns a real NSubstitute substitute for each positive case (assert viaobject is ICallRouterProvider/NSubstitute's own "is this a substitute" check, not a hand-rolled heuristic);UseNSubstitute()/UseNSubstitute(configure)wiring a workingNSubstituteProviderinto a realComposer. - ADR-0025 Amendment 1 regression coverage:
- Negative
IsSubstitutablecases fortypeof(Delegate)andtypeof(MulticastDelegate)themselves (the framework base types), alongside a positive case for a real customdelegatetype — the exact distinctionIsSubclassOf(typeof(MulticastDelegate))exists to draw. - A mutation test: construct
NSubstituteOptions, pass it toUseNSubstitute(...)(or constructNSubstituteProviderdirectly), then mutateSubstituteAbstractClasseson that same instance afterward, and assert the already-registered provider's behavior is unaffected — proving the snapshot, not just that it compiles.
- Negative
- End-to-end composition tests against a real
Composer: an interface parameter composes as a substitute; an abstract class composes as a substitute only when the option allows it (and produces the engine's ordinary "could not satisfy" diagnostic, naming the type, when it doesn't); a delegate parameter composes as a substitute; a[Shared]substitute (viaCompositionRow) is reused by a later nested constructor parameter of the same type — the[Shared] IRepositoryshape from this plan's own Goal section, run for real, not just asserted against a hand-written fake provider (Phase 0 already covers that in isolation). - An API-surface/approval test locking
Compono.NSubstitute's public shape (NSubstituteProvider,NSubstituteOptions,CompositionBuilderExtensions, and nothing else), matchingCompono.XunitV3.Tests' existing pattern (PLAN-0004 Phase 3). - A real end-to-end run through
test/Compono.XunitV3.SampleTests(or a new sibling sample project) provingUseNSubstitute()composes correctly under a real xUnit v3 theory, not justCompono.NSubstitute.Tests' own directComposercalls — matching PLAN-0004 Phase 3's real- packaged-consumer testing strategy, since that's exactly where PLAN-0004 caught a real packaging bug (PrivateAssetson theComponoProjectReference) that aProjectReference-only build couldn't surface.
Phase 3: Docs and cleanup¶
Status: Not Started
-
docs/architecture.md: Resolution Pipeline stage ⅚ rows, the stages-⅘/6/7 summary paragraph, and the "Public provider extensibility" Open Architectural Decisions entry all updated to stop claiming the core mechanism is "not yet implemented"/stages ⅚ are unconditionally "empty" — done early, in response to PR #28 review (Codex, P2): the design-phase wording became stale the moment Phase 0 merged. Still accurately saysCompono.NSubstituteitself doesn't exist yet. -
docs/architecture.md: Providers section still needs the public/internal provider split write-up (ICompositionValueProvideralongside the existing internalICompositionProvidersketch); a Package Boundaries entry forCompono.NSubstitute's realOwnslist — both deferred to this phase since they're additive documentation, not corrections of a false claim. -
docs/mvp.mdMilestone 5 section: links ADR-0024/ADR-0025/PLAN-0005, states ADR-0024's core mechanism is implemented (PLAN-0005 Phase 0) versusCompono.NSubstituteitself not yet — done early, same PR #28 review round. Delegate substitution already in the scope list from the design-phase draft. -
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. -
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." -
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 thedocs/mvp.mdupdate above. -
docs/public-api.md: Naming Vocabulary gainsICompositionValueProvider— already added during the design phase. -
docs/adr/README.md/docs/plans/README.mdindex rows (already added alongside the ADRs/this plan, during the design phase).
Critical Files¶
src/Compono/ICompositionValueProvider.cs,CompositionProviderRequest.cs,CompositionProviderResult.cs(new) — Phase 0.src/Compono/ICompositionProvider.cs,src/Compono/Providers/PublicProviderAdapter.cs(new),CompositionContext.cs,CompositionBuilder.cs,CompositionConfiguration.cs,Composer.cs— Phase 0.src/Compono.NSubstitute/(new project) —NSubstituteProvider.cs,NSubstituteOptions.cs,CompositionBuilderExtensions.cs— Phase 1.test/Compono.NSubstitute.Tests/(new project) — Phase 2.docs/mvp.md,docs/architecture.md,docs/public-api.md— Phase 3.
Test Plan¶
Matches testing.md's existing conventions (xUnit v3 on MTP v2, Arrange-Act-Assert, fixed-seed determinism assertions where relevant, one test project per src project). Per testing.md's "verifying a new public entry point" rule, ICompositionValueProvider/AddSemanticProvider/ AddTestDoubleProvider need their own isolated Compono.Tests coverage (Phase 0) with a hand-written test double, independent of Compono.NSubstitute — the same "core entry point tested in isolation before the package that will really use it exists" pattern PLAN-0004 Phase 0 already established for CompositionRow. Compono.NSubstitute.Tests (Phase 2) then covers the package's own real behavior, plus one real-runner proof (a sample xUnit v3 project, PLAN-0004 Phase 3's precedent) since that specific testing shape is what caught a real packaging bug last milestone.
Breaking-Change Handling During Alpha¶
Per ADR-0024's Alpha Compatibility Policy: Compono is pre-MVP-completion alpha software, and ICompositionValueProvider/CompositionProviderRequest/CompositionProviderResult (and Compono.NSubstitute's own public surface, per ADR-0025) are public but provisional — Milestone 6 and Milestone 7 are expected to validate them further, and a breaking change to any of them is allowed during alpha when real integration/dogfooding evidence justifies it. This plan's own Phase 0/1 work is the first shipment of that contract, not a breaking change to an already-shipped one — nothing below applies to this plan's initial implementation. It applies to any future PR (in this plan's later phases, or after Status: Done) that changes the shape of a contract this plan already shipped.
This repo's established breaking-change mechanism is Release Drafter (.github/release-drafter.yml): its version-resolver maps the breaking (or major) GitHub label directly to a major version bump, distinct from every other category (feat/fix/chore/refactor/deps/docs/ci, which all resolve to a minor or patch bump). There is currently no title/branch-pattern autolabeler rule for breaking (unlike feat/fix/docs/etc., which autolabel from a Conventional-Commit-style PR title prefix) — applying the breaking label to the PR is a manual, deliberate step, which is itself the point: a breaking change to a public contract should never be silently swept into a feat/refactor bump.
A PR that changes an already-shipped piece of this plan's public surface during alpha must:
- Apply the
breakinglabel to the PR (.github/release-drafter.yml'sversion-resolver.majorcategory) so Release Drafter's next draft release produces a major version bump, not a minor/patch one. - Call out the breaking change explicitly in the PR description — what changed, what a consumer needs to do differently, not just "see diff."
- Update the relevant ADR (a dated
## Amendment N (YYYY-MM-DD)section perdesign-decisions.md's Amendment mechanic — never a silent edit to the ADR's original Decision Outcome text),docs/public-api.md, and this plan's own Notes section, all in the same PR — not as follow-up cleanup. - No separate "migration notes" document exists in this repo yet (
docs/mvp.mdis pre-1.0 and has never needed one) — until one does, the PR description plus the ADR's Amendment section together are this repo's record of the change; a future PR is free to introduce a dedicated migration-notes doc if breaking changes during alpha turn out to be frequent enough to warrant one, but that's not designed here.
Open Items¶
- No
Compono.Benchmarksentry for a substitute-composed graph is planned as part of this plan's own exit criteria — worth adding onceCompono.NSubstituteships, to characterize NSubstitute's own proxy- generation cost againstdocs/performance.md's existing baselines, but not required to call this milestone done. - The "NSubstitute itself throws for a superficially-matching type" residual diagnostics gap (ADR-0025's Diagnostics section) is accepted, not solved, in this plan. Revisit only if real usage surfaces this as an actual pain point.
Notes¶
Phase 2 (Done):
test/Compono.NSubstitute.Testscreated (23 tests × 2 TFMs = 46), coveringIsSubstitutabledirectly (IsSubstitutableTests.cs),NSubstituteProvider.TryProvidethrough a realComposer(NSubstituteProviderTests.cs—CompositionProviderResult'sValue/IsHandledare internal toCompono, so a provider's outcome is only observable from outside through the pipeline it feeds, not by callingTryProvidedirectly against a hand-rolledICompositionContext),UseNSubstitute()/UseNSubstitute(configure)wiring (CompositionBuilderExtensionsTests.cs), the[Shared]-via-CompositionRowreuse shape against the realNSubstituteProvider(EndToEndCompositionTests.cs), and aPublicApiSurfaceTestsapproval test. "Is this a substitute" is asserted viaNSubstitute.Core.SubstitutionContext.Current .GetCallRouterFor(value)rather thanis ICallRouterProvider— a real substitute for an interface/abstract-class request is a Castle-proxy instance that does implementICallRouterProvider, but a delegate substitute is a plain compiled delegate whose target isn't the proxy itself, sois ICallRouterProvideris false for exactly the shape this plan's own scope added (delegate substitutes) — confirmed against a small scratch program before writing the assertion helper, not assumed.- Writing this phase's own
IsSubstitutableregression coverage caught a real bug in Phase 1's implementation, before any PR/review round: the negative test fortypeof(Delegate)/typeof(MulticastDelegate)themselves failed, becauseSystem.Delegate/System.MulticastDelegateare themselves abstract, unsealed, non-interface classes —IsSubstitutable's third OR-clause (the abstract-class branch) matched them even though the delegate-specific second clause correctly declined them. ADR-0025 Amendment 1's own stated intent ("the latter wrongly matches the non-substitutable Delegate/MulticastDelegate framework base types themselves") was only half-enforced. Fixed inNSubstituteProvider.IsSubstitutableby excludingtypeof(Delegate).IsAssignableFrom(requestedType)from the abstract-class branch — real delegate types are unaffected (already matched by the earlierIsSubclassOf(MulticastDelegate)clause, which returns before the abstract branch is ever reached for them). - The real end-to-end run (this plan's own last Phase 2 task) surfaced a second, larger gap: this plan's own Goal-section snippet (
[Shared] IOrderRepository repositoryas a bare xUnit theory parameter) failed to compile withCMP0003when actually built against a real packagedCompono.XunitV3.SampleTestsconsumer.Compono.Generators'LeafTypeClassifier.IsRuntimeProviderResolved— the root-only classifier deciding whetherComposer.Create<T>()'s (or aCompositionRow's own) requested type needs a generated plan at all — predates ADR-0024 and still hard-excluded interface/ abstract-class/delegate roots, on the (once-true, now-stale) assumption that nothing could ever satisfy such a root at runtime. Fixed by makingIsRuntimeProviderResolveddelegate to the member-levelIsProviderResolvedcheck, so a root is classified identically to a member — see ADR-0024's Amendment 2 for the full account, including why this ships as an ordinaryfix(additive compile-time behavior, not a breaking change to an already-shipped contract) rather than under thebreakinglabel. Confirmed via user decision before implementing (the alternative considered was leavingCMP0003as-is and working around it in the sample test, matchingDomain.cs's existingGatewayConsumerpattern for the same limitation — rejected in favor of fixing the actual stale assumption, since real dogfooding evidence now contradicts it). Four existingCompono.Generators.Testssnapshot tests that asserted the old (now-incorrect)CMP0003behavior were rewritten to assert clean compilation instead; one new regression test (ConcreteRootTypeWithNoAccessibleConstructor_StillReportsDiagnostic_AfterRootProviderCheck) proves the loosening is scoped to interface/abstract-class/delegate roots only — a concrete type's root classification is unchanged. test/Compono.XunitV3.SampleTestsextended with aCompono.NSubstitutepackage reference (added toDirectory.Packages.props' central version management, matchingCompono.XunitV3's existingVersion="1.0.0"local-feed convention), a thirddotnet packcall threaded throughpack-to-local-feed.shand itsPackToLocalFeedMSBuild target, and a newNSubstituteTests.csrunning this plan's own Goal scenario verbatim (CreateOrderHandler/IOrderRepository/[Shared]/UseNSubstitute()via anICompositionProfile) against the real packaged dependency chain, per PLAN-0004 Phase 3's real-packaged-consumer strategy — the same strategy that caught PLAN-0004's ownPrivateAssetspackaging bug, and caught this phase'sCMP0003gap here.- Full suite re-verified green after both fixes: whole-solution
dotnet build/dotnet test720/720 (Compono.Tests412/412,Compono.Generators.Tests168/168,Compono.XunitV3.Tests94/94,Compono.NSubstitute.Tests46/46 — all ×2 TFMs), plus a separate realdotnet testrun oftest/Compono.XunitV3.SampleTestsagainst packages packed from current source (notProjectReference), confirmingNSubstituteTests.Saves_orderpasses end-to-end.
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, orCompositionBuilderExtensions. Compono.NSubstitute.csprojmirrorsCompono.XunitV3.csproj's shape:net10.0;net11.0TFMs,ProjectReferencetoComponowithPrivateAssets="none"(PLAN-0004 Phase 3's packaging lesson, applied proactively rather than rediscovered),PackageReferencetoNSubstitute(version centrally managed, already present inDirectory.Packages.propsfrom the test-project usage), andInternalsVisibleTofor the not-yet-createdCompono.NSubstitute.Tests(Phase 2) soIsSubstitutablecan stayinternal.docs/architecture.md's package-dependency diagram already listsCompono.NSubstitute → Compono, NSubstitute— matched exactly, no reference to any other package.- Added to
Compono.slnx. Whole-solutiondotnet buildgreen, no warnings. - No test project yet (Phase 2) —
IsSubstitutable/NSubstituteProvider/UseNSubstituteare 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 anddocs/mvp.md's Milestone 5 section both still saidCompono.NSubstitutewas "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 direction — Phase 1-3 remain separate PRs, per
design-decisions.md's phase rule. - The 3-arg
CompositionContext(seed, registrations, serviceProvider)test seam (used byCompositionRegistrationTests) also forwards into the extended constructor chain and needed its ownsemanticProviders: []/testDoubleProviders: []update — not called out explicitly in the plan's task wording (only the 5-arg/6-arg constructors were), but the same mechanical change threaded one level further down the existing constructor-forwarding chain. CompositionValueProviderTests(new file,test/Compono.Tests/CompositionValueProviderTests.cs) covers the publicComposer-level surface (AddSemanticProvider/AddTestDoubleProvider, stage precedence, registration order,NotHandledfallthrough, diagnostics identity through the adapter, shared-scope reuse, uncaught provider exceptions) — the shared-scope-reuse test uses the internalCompositionContext(...)test seam directly (wrapping the test double inPublicProviderAdapterexplicitly, since that seam takes internalICompositionProvider, not the publicICompositionValueProvider) to reachResolveSharedForTesting, the same seamCompositionScopeTests/CompositionDiagnosticsTestsalready use for scope-level assertions with no public[Shared]entry point available at theComponocore layer.- Full suite green:
Compono.Tests408/408 (204 × 2 TFMs — 196 pre-existing - 8 new),
Compono.Generators.Tests166/166 (unaffected),Compono.XunitV3.Tests94/94 (unaffected) —dotnet build/dotnet testagainst the whole solution, not just the new test file. - PR #28 review (Codex, two P1 findings) caught a real gap in this phase's first pass, fixed before merge — see ADR-0024 Amendment 1 for the full account. A provider calling the descriptor-less
context.Resolve<T>()threw unconditionally (no manual-resolve frame was ever pushed for public-provider dispatch), and a provider recursing on its own requested type had no cycle protection at all and ran untilStackOverflowException. Fixed with a new internalCompositionContext.InvokeProvidermethod — structurally parallel toInvokeFactorybut reentrance-keyed on(provider instance, requested type)rather than delegate identity, and deliberately not reusingInvokeFactory's exception-wrapping catch block, to keep ADR-0024's Provider Failure Semantics decision (uncaught propagation) intact.PublicProviderAdapternow carries its ownPipelineStage(set at construction inCompositionBuilder.Build()) soInvokeProvider's reentrance-failure trace entry is recorded against the right stage. Two new regression tests added; the pre-existingThrownException_FromAPublicProvider_PropagatesUncaughttest continues to pass unchanged. Full suite re-verified green after the fix: 672/672 across the whole solution (Compono.Tests412/412 = 206 × 2 TFMs). - A second PR #28 review round (Codex, two P2 findings) caught doc/API wording that went stale the moment the fixes above merged. (1)
docs/architecture.md/mvp.md/public-api.mdstill described the provider extension point as "design only"/stages ⅚ as unconditionally "empty" — no longer true for the core mechanism, only forCompono.NSubstituteitself; pulled forward from this plan's own Phase 3 task list rather than left contradicting the code until that phase lands (see Phase 3's task list above for exactly what was pulled forward versus what's still open there). (2)ICompositionContext.Resolve<TValue>()'s public XML doc and itsInvalidOperationExceptionmessage still described a factory-only contract, not mentioning that a provider'sTryProvide(via theInvokeProviderfix above) is now also a valid caller — updated both, plus the same "factory-only" framing onPathSegment.ManualResolve/CompositionRequestKind.ManualResolve's XML docs for consistency. Full suite re-verified green: 672/672.
ADR-0024/ ADR-0025 reached Accepted on 2026-07-31, after design review confirmed the provider contract should be scoped to the architectural guarantees listed in ADR-0024's Decision Outcome (named interface, small decoupled request/ result contract, deterministic stage placement/order, isolation from internal engine types, logical-provider diagnostics identity, immutable reusable registration, NotHandled/Handled semantics) rather than over-designed for hypothetical post-alpha stability — see ADR-0024's Alpha Compatibility Policy. No task below is checked off yet; implementation has not started.