[ADR-0039] Future Extension Package Admission Gate and Release Sequence¶
Status: Accepted
Date: 2026-08-11
Decision Makers: Nick Cipollina, Claude (design deep dive)
Context¶
Future Packages lists natural candidate extension packages that aren't designed or committed yet: additional test-framework integrations, additional test-double integrations, and a richer Microsoft.Extensions.DependencyInjection integration. None of these has a concrete design or a real-demand trigger — per ADR-0029's evidence-over-prediction bias, a candidate only becomes real roadmap content once real demand and a concrete design exist.
This ADR's first version (2026-08-11, revised in place below rather than superseded by a new number — the ADR was still Proposed, so the numbering/immutability rule that protects an Accepted ADR's original text does not apply yet) proposed a three-item OR-gate ("supply composed values, or expose composed values naturally to a test framework, or bridge an established registration system") plus a committed six-package release sequence (Compono.TUnit, Compono.NUnit, Compono.MSTest, Compono.FakeItEasy, Compono.Moq, Compono.DependencyInjection, in that order, TUnit first because its source-generated test model is architecturally close to Compono's own).
A deep design dive against that first version — inspecting the three shipped integration packages (Compono.XunitV3, Compono.NSubstitute, Compono.Bogus), the ADRs that established their extension points (ADR-0021, ADR-0024, ADR-0025), the DI boundary ADR-0019 already draws, and the current public extension mechanisms of all six candidate ecosystems (TUnit, NUnit, MSTest, FakeItEasy, Moq, Microsoft.Extensions.DependencyInjection) — found two things wrong with that first version, both corrected below:
- The three-item gate describes which shape of package a candidate is, not whether it deserves to exist. It's an OR of near-tautologies: almost any wrapper around a test library can claim to "supply composed values." It cannot actually reject a candidate that exists only because its underlying library is popular — the exact failure mode this ADR exists to prevent (
Compono.TUnitexisting because TUnit exists, regardless of whether TUnit's extension surface offers anything Compono-specific to compose against). - The committed sequence has no evidentiary basis.
docs/roadmap/post-mvp.mdrecords that two real dogfooding passes under ADR-0029 (cosmere-tracker,trivia-platform) have already run and surfaced zero outstanding roadmap candidates in the test-framework/test-double/DI space. Every one of the six named candidates has zero evidence today. Recording a specific six-package order this far ahead of any real signal is prediction dressed as planning — directly against ADR-0029's own rationale, not a neutral non-binding note.
This revision replaces the OR-gate with a two-stage admission model and drops the committed sequence, while keeping the parts of the original proposal that held up under research: a standing gate is worth having, and some candidates (see Decision Outcome) do have a real, non-wrapper integration surface worth recording even before evidence exists for them.
Decision Drivers¶
docs/architecture/design-principles.md's bias against scope creep and against becoming "a monolithic testing toolkit" / "a feature-complete wrapper over every third-party integration" — the package set should grow deliberately, not by accretion, and a gate that can't reject a branding wrapper doesn't actually protect against either anti-goal.- ADR-0029's evidence-over-prediction bias — sharpened by this revision's research finding that zero of the six candidates currently has any evidence at all (
docs/roadmap/post-mvp.md). A gate can record that a candidate is architecturally legitimate without pretending that also means it's due. - The existing extension-point pattern (
Compono.NSubstitute/Compono.BogusonICompositionValueProviderper ADR-0024;Compono.XunitV3onCompositionRow/CreateRowper ADR-0021) — every shipped package builds on a public extension point that was itself designed by its own core-extension ADR before the package's own design ADR. A future package's admission has to be judged against this real precedent, not an abstract standard. - ADR-0019 already drew an explicit boundary for
Microsoft.Extensions.DependencyInjection: core ships a thinIServiceProviderfallback only; a "richer" DI integration is explicitly out of scope for core and was always expected to be a separate package built the same way NSubstitute/Bogus are. - Skill-maintenance cost is real but, per direct inspection of
skills/compono's existing detection-table + per-package-reference-file pattern, linear and small per additional package — this driver should be weighed in the gate, not treated as a standalone blocker.
Considered Options¶
- No formal admission gate — evaluate each future package candidate independently, whenever real demand surfaces, with no standing criteria.
- A single OR-gate (any one of three conditions) applied to every future candidate, plus a committed candidate release sequence — this ADR's original 2026-08-11 proposal.
- A two-stage admission model (an architectural "shape" gate this ADR owns, feeding into ADR-0029's existing evidence gate), with no committed sequence — only non-binding heuristics for candidates that clear both stages simultaneously.
Decision Outcome¶
Chosen option: Option 3 — a two-stage admission model, no committed release sequence.
Why not Option 1 (no gate)¶
Rejected for the same reason the original proposal rejected it: without a standing bar, a low-value candidate can reach a design pass before its lack of value is obvious, and — per this revision's research — the concrete case (Compono.TUnit existing because TUnit exists) is real enough to name explicitly, not hypothetical.
Why not Option 2 (this ADR's own first version)¶
Rejected on the two grounds in Context above: the OR-gate can't actually reject a branding wrapper, and the committed sequence had no evidentiary support once checked against docs/roadmap/post-mvp.md's actual dogfooding record.
The two-stage admission model¶
Gate A — architectural admission (this ADR). A candidate must clear all of the following, not just one, before it's treated as an admitted candidate:
- Compono-specific value. It solves meaningful composition-related friction, not branding or convenience around an already-easy call.
- Native ecosystem fit. The resulting API is idiomatic in the integrated ecosystem's own terms — not a
Compono.XunitV3-shaped clone bolted onto a different framework's extension model. - Meaningful abstraction. Consumers get materially more than a trivial extension method they could write themselves in an afternoon.
- Architectural fit. It can be built entirely on an existing public extension point, or on a to-be-designed extension point this ADR names explicitly as a prerequisite — never on a core change invented ad hoc during the package's own design pass.
- Package-boundary justification. The dependency genuinely belongs outside core, and is substantial enough to be its own package rather than a documentation recipe or an addition to an existing package.
Maintenance/CI/docs/skill cost is a weighing factor across these five, not a sixth pass/fail condition on its own — per the skill-cost finding above, that cost is real but small for this repo's existing routing pattern, and shouldn't by itself veto a candidate that clears the five bars above.
A candidate that fails Gate A doesn't get a Proposed ADR of its own regardless of demand — the same consequence the original OR-gate stated, now backed by a gate that can actually produce that verdict for a case like Compono.TUnit exists because TUnit exists.
Gate B — evidence admission (unchanged, ADR-0029). A candidate that clears Gate A is an admitted candidate, not yet roadmap content. It still needs a real-demand trigger — dogfooding friction, repeated consumer request, or another concrete signal per ADR-0029's rubric — before it becomes a roadmap item with its own problem-focused Proposed ADR. This ADR does not relax, restate, or duplicate ADR-0029's evidence rubric; Gate A and Gate B are answering different questions (could this be a legitimate Compono package vs. is there real reason to build it now) and both must clear, in that order, before a design pass starts.
Terminology (per this repo's own request to keep these distinct)¶
- Candidate — named in
docs/roadmap/future-packages.md, not yet evaluated against Gate A. - Admitted candidate — cleared Gate A: architecturally legitimate, still no evidence. Recorded in
future-packages.md, not in aProposedADR of its own. - Roadmap item — cleared Gate B too: real evidence exists. Gets a problem-only
ProposedADR per ADR-0029, listed indocs/roadmap/post-mvp.md. - Committed implementation work — the roadmap item's ADR reaches
Acceptedand aPlanmovesIn Progress.
Nothing in this ADR moves any candidate past stage 2. Reaching stage 3 for any of them requires real evidence this ADR cannot manufacture or predict.
Candidate-by-candidate Gate A disposition¶
Evaluated against each ecosystem's current (2026) public extension mechanisms, researched directly rather than assumed:
Compono.TUnit— admitted candidate. TUnit'sIDataSourceAttributefamily (UntypedDataSourceGeneratorAttributein particular — TUnit's own docs cite AutoFixture-shaped libraries as the motivating case for it), its per-rowTestBuilderContext, its combinatorial interplay with[Arguments], and itsDependencyInjectionDataSourceAttribute-style class-constructor composition path together give a real, richer-than-xUnit-v3 integration surface. The original TUnit-first rationale (source-gen architectural kinship) does not survive scrutiny and is retired here — TUnit being source-generated is not, on its own, consumer value; the actual justification is this concrete data-source extension surface, unrelated to whether TUnit's own test model happens to be source-generated.Compono.NUnit— admitted candidate.IParameterDataSourceoffers genuine per-parameter composition granularityCompono.XunitV3's row model doesn't have, andITestBuilder/IFixtureBuildercover the row/fixture-constructor cases. A real, distinct integration shape, not a clone ofCompono.XunitV3.Compono.MSTest— admitted candidate, weakest of the three.ITestDataSourceis a stable, long-standing extension point (unchanged in shape since MSTest v1.2, still current in v4.3), but it's thin: synchronous only, no per-row context object, no combinatorial engine to interoperate with. It still clears "meaningful abstraction" — composed values as named test parameters plus aGetDisplayNamehook for seed disclosure is real value over an in-bodyComposer.Create<T>()call — but it's the least distinctive of the three test-framework candidates.Compono.FakeItEasy— does not clear Gate A today; downgraded to a documentation-only idea.FakeItEasy.Sdk.Create.Fake(Type)is a real, clean extension point, but the resulting package would be ~80% structurally identical toCompono.NSubstitute(sameICompositionValueProvidershape, sameNotHandled/Handledsplit, same static substitutability predicate), and no dogfooding pass in this repo's own history has ever surfaced friction pointing at FakeItEasy over NSubstitute. This fails "meaningful abstraction" relative to a package that already exists, not because FakeItEasy's own API is thin. Recorded infuture-packages.mdas a documentation recipe ("how to write your ownICompositionValueProviderfor FakeItEasy," followingCompono.NSubstitute's published shape almost line for line) rather than a package candidate — promote back to a package candidate only if that recipe itself surfaces real friction or demand.Compono.Moq— deferred indefinitely, blocked on maintenance health, not TFM compatibility. An earlier draft of this ADR claimed Moq's lack of anet8.0/net9.0-specific target made it TFM-incompatible with Compono's own floor — that claim is wrong and is retracted here: Moq shipsnetstandard2.0/netstandard2.1assets, and ADR-0037 (its "netstandard2.1 Compatibility" section) already documents that anet8.0/net9.0project consumes anetstandard2.0/netstandard2.1-only dependency through NuGet's own asset-compatibility fallback without issue — anet8.0/net9.0consumer can restore and use Moq today. The actual, narrower basis for deferral: Moq has shipped no release in roughly 23 months and carries durable reputational damage from the 4.20.0 SponsorLink incident — a workable, if reflection-heavier, integration surface (Moq's API is otherwise a structural cousin of NSubstitute's) doesn't overcome a dependency this stale. Recorded as an explicit deferral with a re-evaluation trigger (Moq resumes active, regular releases), not a silent drop —future-packages.mdstates the reason so it isn't mistaken for lost interest.Compono.DependencyInjection— does not clear Gate A today as a package; downgraded to a documentation-only idea pending a prerequisite core ADR. Research into currentMicrosoft.Extensions.DependencyInjection(net8/net9/net10-era) surfaces exactly two ideas that would need real Compono-specific bridging: keyed service resolution (GetKeyedService/AddKeyedSingleton, .NET 8+) and DI-scope ownership for a composition. Both require a core concept that doesn't exist yet — a keyed/namedCompositionProviderRequest, and a composition-scope-owns-DI-scope lifetime model — which is itself a future core-extension ADR in the shape of ADR-0021/ADR-0024, not something a package's own design pass should invent ad hoc (this fails Gate A's "architectural fit" leg exactly as written: buildable on an existing or explicitly-named extension point, not one invented during the package's own design). Every other "richer DI integration" idea (auto-registration sugar, descriptor-driven validation) is achievable by a consumer today in a handful of lines against the existingUseServiceProvider(...)fallback (ADR-0019) and doesn't need a package at all. Recorded infuture-packages.mdas an idea explicitly gated behind that prerequisite core design, not a named package in any sequence. If it's ever designed,Compono.DependencyInjectionremains the correct name — ADR-0019 already anticipated it under this name, and it's consistent with theCompono.<EcosystemName>pattern.
No committed release sequence¶
Unlike the original proposal, this ADR records no candidate order. docs/roadmap/post-mvp.md's finding that two real dogfooding passes have already produced zero outstanding roadmap candidates in this entire space means every admitted candidate above is equally without evidence today — there is no principled basis for ranking them against each other before Gate B has anything to rank.
Non-binding heuristics only, to apply if and when more than one candidate clears Gate B at roughly the same time: prefer higher Compono-specific value relative to ongoing maintenance cost, and prefer whichever candidate exercises a meaningfully different part of Compono's architecture (validating a distinct extension point) over one that merely repeats an already-proven pattern. Category completion — finishing all test-framework integrations before starting a test-double integration, or vice versa — is explicitly rejected as a sequencing principle. It has no grounding in evidence or architecture; it's aesthetic completionism, and ADR-0029's whole framework exists to keep decisions like this tied to real signal instead.
Implementation-readiness note (not a Gate A dimension)¶
Both Compono.XunitV3 (row-binding/materialization logic) and Compono.NSubstitute (the interface/delegate/abstract-class substitutability predicate) currently hold logic that a second package in their respective category would otherwise duplicate. This doesn't affect either candidate's Gate A disposition, but whichever admitted candidate is the first in its category to actually reach Gate B should extract that shared logic to an internal location as part of its own design pass, rather than cloning it. Recorded here so it isn't rediscovered from scratch; not itself a design decision.
Positive Consequences¶
- Gate A can actually produce a "no" for a candidate that exists only because its underlying library is popular, which the original OR-gate structurally could not.
- The candidate-by-candidate Gate A pass gives
future-packages.mdan honest, evidence-checked disposition for all six original candidates instead of leaving them as an undifferentiated list. - No sequence is recorded that a later design pass would have to justify ignoring — the sequence question is deferred entirely to whenever real evidence exists to answer it.
- The two-stage terminology (candidate / admitted candidate / roadmap item / committed implementation work) gives every future discussion of "is X real yet" an unambiguous answer.
Negative Consequences¶
- This revision spends more up-front analysis than the original three-item gate did, for candidates that may never clear Gate B at all — accepted, because the alternative (a gate that can't reject a branding wrapper) is the exact failure mode this ADR exists to prevent.
Compono.FakeItEasyandCompono.DependencyInjectionare downgraded from named package candidates to documentation-only ideas without either having been given its own design pass — accepted, because the research behind both downgrades is recorded explicitly above (structural identity to an existing package; a real core prerequisite that doesn't exist yet), not asserted without reasoning, and either can be reopened the moment new evidence or a prerequisite ADR changes the picture.Compono.Moq's deferral is contingent on an external project's release cadence, outside this repo's control — accepted, with an explicit re-evaluation trigger recorded rather than a silent drop.
Pros and Cons of the Options¶
No formal admission gate¶
- Good, because it defers all criteria to each candidate's own future design pass, when more context exists.
- Bad, because without a standing bar, a low-value candidate could reach a design pass before its lack of value is obvious — and this revision's research shows that's not hypothetical (
Compono.FakeItEasy,Compono.Moqboth would have looked superficially admissible under no gate at all).
Single OR-gate + committed sequence (this ADR's original 2026-08-11 proposal)¶
- Good, because it records both a qualification bar and sequencing rationale in one place, with a stable linkable identity.
- Bad, because the OR-gate cannot reject a candidate that merely claims to "supply composed values" — it describes package shape, not merit.
- Bad, because the committed sequence had no evidentiary basis once checked against
docs/roadmap/post-mvp.md's actual dogfooding record — zero candidates have evidence today, so ranking them was prediction, not planning.
Two-stage admission model, no committed sequence (chosen)¶
- Good, because Gate A can produce a real "no" (demonstrated concretely against
Compono.FakeItEasy/Compono.Moq/Compono.DependencyInjectionabove) while still admitting genuinely distinct integrations (Compono.TUnit/Compono.NUnit/Compono.MSTest). - Good, because it keeps Gate A (architectural legitimacy) and Gate B (evidence) as separate questions rather than conflating them, matching how ADR-0029 already frames "roadmap candidate" as its own gate.
- Good, because it doesn't ask this ADR to predict an order no evidence yet supports.
- Bad, because a reader wanting "just tell me what's next" gets no ranked answer — accepted, since ADR-0029's own framework says that answer doesn't exist yet, and pretending otherwise is the thing being corrected.
Links¶
- Future Packages — the roadmap page this ADR is proposed from, and where each candidate's Gate A disposition above should be reflected.
- Post-MVP Roadmap — the evidence record showing zero outstanding roadmap candidates from two real dogfooding passes, the finding that eliminated this ADR's original committed sequence.
- ADR-0029 — Gate B, unchanged by this ADR; the evidence-over-prediction bias this revision is scoped to actually honor, not just avoid contradicting.
- ADR-0024, ADR-0025, ADR-0021, ADR-0022 — the existing extension-point pattern every Gate A disposition above is judged against.
- ADR-0019 — the DI boundary this ADR's
Compono.DependencyInjectiondisposition builds on directly; already anticipated this package's name and shape. - ADR-0037 — documents the NuGet asset-compatibility fallback that makes Moq's
netstandard2.0/netstandard2.1assets consumable fromnet8.0/net9.0; the reasonCompono.Moq's deferral below is grounded in maintenance health, not TFM incompatibility.