Skip to content

feat(core): milestone 5 phase 0 - public provider extensibility engine wiring - #28

Merged
ncipollina merged 3 commits into
mainfrom
feat/milestone-5-phase-0-provider-extensibility
Jul 31, 2026
Merged

feat(core): milestone 5 phase 0 - public provider extensibility engine wiring#28
ncipollina merged 3 commits into
mainfrom
feat/milestone-5-phase-0-provider-extensibility

Conversation

@ncipollina

Copy link
Copy Markdown
Contributor

📋 Summary

Milestone 5 (NSubstitute Integration) design docs — ADR-0024 (public provider extensibility model), ADR-0025 (Compono.NSubstitute package design), PLAN-0005 (four-phase implementation plan) — plus PLAN-0005 Phase 0: the core engine wiring both ADRs specify. Phase 0 fills in pipeline stages 5/6 (semantic value providers, test-double providers), which have been wired end-to-end since Milestone 2 but never fed anything until now, with a small public extension point decoupled from the engine's own internal request/provider types.


📝 Changes

Design docs

  • ADR-0024 — the core public provider extensibility model: a named ICompositionValueProvider interface (not a bare delegate — a genuine fork raised and resolved during design review), a small decoupled request/result contract, deterministic stage placement/registration order, diagnostics identity, immutable/reusable registration, NotHandled/Handled semantics. Includes an explicit Alpha Compatibility Policy: the contract is public but provisional during alpha, revisable when Milestone 6/7 or real usage justifies it, any such change recorded as a dated ADR Amendment rather than a silent edit.
  • ADR-0025Compono.NSubstitute's own package design built on ADR-0024 (substitutable-shape rules for interfaces/delegates/abstract classes, NSubstituteOptions, diagnostics). Includes Amendment 1, recorded during pre-implementation review: NSubstituteProvider must snapshot NSubstituteOptions rather than retain the caller-owned mutable instance, and delegate matching must use IsSubclassOf(typeof(MulticastDelegate)) rather than Delegate.IsAssignableFrom (which wrongly matches the two framework base types themselves).
  • PLAN-0005 — the four-phase implementation tracker (Phase 0: core engine extension point; Phase 1: Compono.NSubstitute; Phase 2: test suites/verification; Phase 3: docs), including a Breaking-Change Handling During Alpha section sourced from this repo's actual Release Drafter config (.github/release-drafter.yml's breaking/major label → major version bump).

Phase 0 implementation (src/Compono)

  • New public surface: ICompositionValueProvider (TryProvide), CompositionProviderRequest/CompositionProviderResult, CompositionBuilder.AddSemanticProvider/AddTestDoubleProvider.
  • Compiled at CompositionBuilder.Build() time into an internal PublicProviderAdapter : ICompositionProvider — the same "public builder data compiles into an internal provider" shape ADR-0020's .For<T>() rules already use. No new pipeline mechanism: stage order, shared-scope reuse, and recursion detection are all reused unchanged.
  • ICompositionProvider gains a ProviderType default-interface member so diagnostics keep naming the real, logical provider through the adapter (never the adapter's own type).
  • Composer.Create<T>/CreateMany<T>/CreateRow (and one internal test seam one level further down the constructor chain) now thread the configured semantic/test-double provider lists into CompositionContext.

Tests

  • New CompositionValueProviderTests.cs, exercising the public Composer API in isolation from Compono.NSubstitute (which doesn't exist yet — testing.md's "verify a new public entry point in isolation" rule): stage precedence (semantic before test-double), registration order, NotHandled fallthrough, diagnostics identity through the adapter, shared-scope reuse, uncaught-provider-exception propagation.

🧪 Validation

  • Build/test status: dotnet build/dotnet test against the whole solution — 668/668 passing, 0 warnings. (Compono.Tests 408/408 = 204 × 2 TFMs, Compono.XunitV3.Tests 94/94, Compono.Generators.Tests 166/166.)
  • Manual verification performed: confirmed no pre-existing test broke (196 pre-existing Compono.Tests cases still pass unchanged); confirmed docs/mvp.md/architecture.md/public-api.md are deliberately left describing stages 5/6 as still empty — that update is explicitly PLAN-0005 Phase 3's job, matching PLAN-0004's precedent of deferring doc updates to the final phase.
  • Edge cases checked: multiple providers registered into the same stage are tried in registration order; a public provider's thrown exception propagates uncaught (no new exception-wrapping behavior introduced); a shared request satisfied by a public provider is reused by a later ordinary request via the existing scope mechanism, with zero new code.

💬 Notes for Reviewers

  • This PR implements only PLAN-0005 Phase 0 (core engine wiring). Compono.NSubstitute itself (Phase 1) is a separate, later PR, per this repo's one-phase-per-PR rule.
  • Both ADRs are Accepted. ADR-0025's Amendment 1 was applied before any Compono.NSubstitute code exists, so there's no drift between the ADR and a future implementation to reconcile later.

Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com

…e wiring

Design docs for Milestone 5 (NSubstitute Integration): ADR-0024 (the core
public provider extensibility model - how an integration package
contributes open-ended, pattern-matching composition logic to pipeline
stages 5/6, deliberately deferred by Milestone 3), ADR-0025
(Compono.NSubstitute's own package design, built on ADR-0024), and
PLAN-0005 tracking the milestone's four phases. Both ADRs record an
explicit Alpha Compatibility Policy - the contract is public but
provisional during alpha, revisable when Milestone 6/7 or real usage
justifies it, any such change recorded as a dated ADR Amendment rather
than a silent edit.

Implements PLAN-0005 Phase 0 against ADR-0024's Decision Outcome:

- New public surface: ICompositionValueProvider (TryProvide), the
  decoupled CompositionProviderRequest/CompositionProviderResult value
  types, and CompositionBuilder.AddSemanticProvider/AddTestDoubleProvider
  - registering into pipeline stages 5/6, which have been wired end to
  end since Milestone 2 but never fed anything until now.
- Compiled at CompositionBuilder.Build() time into an internal
  PublicProviderAdapter : ICompositionProvider, the same "public builder
  data compiles into an internal provider" shape ADR-0020's .For<T>()
  rules already use - no new pipeline mechanism, stage order/shared-scope
  reuse/recursion detection are all reused unchanged.
- ICompositionProvider gains a ProviderType default-interface member so
  diagnostics keep naming the real, logical provider (e.g. a future
  NSubstituteProvider) through the adapter, never the adapter's own type.
- Composer.Create<T>/CreateMany<T>/CreateRow (and the internal 3-arg test
  seam one level further down the constructor chain) now thread the
  configured semantic/test-double provider lists into CompositionContext.

New CompositionValueProviderTests.cs covers this end to end through the
real Composer API, in isolation from Compono.NSubstitute (which doesn't
exist yet, per testing.md's "verify a new public entry point in
isolation" rule): stage precedence, registration order, NotHandled
fallthrough, diagnostics identity through the adapter, shared-scope reuse,
and uncaught-provider-exception propagation.

Full suite green: Compono.Tests 408/408 (204 x 2 TFMs), Compono.XunitV3.Tests
94/94, Compono.Generators.Tests 166/166 - dotnet build/dotnet test against
the whole solution, zero warnings, zero regressions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions github-actions Bot added the feat label Jul 31, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cee71a5787

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Compono/ICompositionValueProvider.cs Outdated
Comment thread src/Compono/Providers/PublicProviderAdapter.cs Outdated
@safe-settings-sync safe-settings-sync Bot removed the feat label Jul 31, 2026
…-recursion

PR #28 review (Codex, two P1 findings, same root cause): PublicProviderAdapter
called a wrapped ICompositionValueProvider's TryProvide directly, bypassing
CompositionContext.InvokeFactory entirely - the only thing that pushes a
manual-resolve frame or guards against reentrance.

1. A provider calling the descriptor-less context.Resolve<T>() - exactly what
   ICompositionValueProvider's own XML doc promises works - threw
   InvalidOperationException unconditionally, since no manual-resolve frame was
   ever active.
2. A provider recursing on its own requested type (via the descriptor-based
   overload, which needs no frame) had no cycle protection and ran until
   StackOverflowException instead of producing a diagnosed CompositionException.

Fixed with a new CompositionContext.InvokeProvider method, structurally
parallel to InvokeFactory but distinct in two ways: reentrance is keyed on
(provider instance, requested type), not delegate identity - TypeRuleProvider/
MemberRuleProvider get away with delegate-identity keying because each
instance is compiled 1:1 for exactly one type, but one ICompositionValueProvider
instance can legitimately handle many types (e.g. "any interface"), including
composing a different type as a legitimate nested dependency, which a
provider-only key would have wrongly blocked. And it never catches or wraps an
exception TryProvide throws - InvokeFactory's blanket catch was deliberately
not reused, since ADR-0024's Provider Failure Semantics commits to a provider's
own thrown exception propagating uncaught; the reentrance guard's own diagnosed
exception is a different concern from wrapping the provider's exception, and
stays intact.

PublicProviderAdapter now carries its own PipelineStage (set at construction in
CompositionBuilder.Build) so InvokeProvider's reentrance-failure trace entry is
recorded against the right stage.

Recorded as ADR-0024 Amendment 1 (the ADR is Accepted, so this is a dated
amendment, not a silent edit to its original Decision Outcome text) and in
PLAN-0005's Phase 0 Notes. Two new regression tests
(CompositionValueProviderTests.cs); the pre-existing
ThrownException_FromAPublicProvider_PropagatesUncaught test continues to pass
unchanged, confirming the fix didn't reverse the Provider Failure Semantics
decision. Full suite green: 672/672 across the whole solution.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions github-actions Bot added the feat label Jul 31, 2026
@ncipollina

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3cb7892b51

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread docs/architecture.md Outdated
Comment thread src/Compono/CompositionContext.cs
…ry-only wording

PR #28 review (Codex, two P2 findings):

1. docs/architecture.md/mvp.md/public-api.md still described the public
   provider extension point as "design only" and stages 5/6 as
   unconditionally "empty" - stale the moment the prior commit's
   InvokeProvider fix landed, since AddSemanticProvider/AddTestDoubleProvider
   are real, tested public API now. Updated the Resolution Pipeline table,
   the stages-4/5/6/7 summary paragraph, the Open Architectural Decisions
   entry, mvp.md's Milestone 5 section, and public-api.md's Provider
   Extensibility section to distinguish "ADR-0024's core mechanism:
   implemented" from "Compono.NSubstitute (ADR-0025): still doesn't exist" -
   pulled forward from PLAN-0005's own Phase 3 task list rather than left
   contradicting the code until that phase lands; Phase 3's checklist
   updated to reflect what was pulled forward versus what's still open.

2. ICompositionContext.Resolve<TValue>()'s public XML doc and its
   InvalidOperationException message still described a factory-only
   contract, not mentioning that a provider's TryProvide (via the new
   InvokeProvider method) is now also a valid caller. Updated both, plus
   the same factory-only framing on PathSegment.ManualResolve/
   CompositionRequestKind.ManualResolve's XML docs for consistency.

Full suite green: 672/672.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@ncipollina
ncipollina merged commit 630d9e1 into main Jul 31, 2026
7 checks passed
@ncipollina
ncipollina deleted the feat/milestone-5-phase-0-provider-extensibility branch July 31, 2026 18:46
ncipollina added a commit that referenced this pull request Jul 31, 2026
…nted

docs/public-api.md and docs/mvp.md still said the package was "not yet
implemented" after this phase's code landed - pulled the correction
forward into this PR rather than deferring to Phase 3, per Codex review
and matching the same fix pattern PR #28 established for Phase 0.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ncipollina added a commit that referenced this pull request Jul 31, 2026
…29)

* feat(nsubstitute): milestone 5 phase 1 - Compono.NSubstitute package

NSubstituteProvider/NSubstituteOptions/UseNSubstitute per ADR-0025
(Amendment 1's corrected snapshot/delegate-matching shapes). Tests
(Phase 2) and docs (Phase 3) tracked separately in PLAN-0005.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(xunitv3): shorten PrivateAssets comment on Compono reference

Trim wording that implied all transitive dependencies would otherwise
disappear - it's specifically the analyzer asset that needs to flow through.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(nsubstitute): stop describing Compono.NSubstitute as not implemented

docs/public-api.md and docs/mvp.md still said the package was "not yet
implemented" after this phase's code landed - pulled the correction
forward into this PR rather than deferring to Phase 3, per Codex review
and matching the same fix pattern PR #28 established for Phase 0.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant