[PLAN-0059] Compono.NUnit Package Design — Implementation Plan¶
Status: Done
Implements: ADR-0059 (Compono.NUnit package: TestAttribute-based, ITestBuilder- implementing [Compose] attribute family — no [TestFixture] requirement, revised pre-acceptance — NUnit-only dependency at [3.14.0, 5.0.0), CompositionRow/RowInvokerRegistry reused unchanged, the BindingPlan/RowInvokers pattern adapted package-locally with an IMethodInfo→MethodInfo unwrap, NUnitTestCaseBuilder/TestCaseParameters-based TestMethod construction, no partial-row merging with [TestCase]/[Values]/ [Range] — each independent, none merged, none unused — no disposal ownership)
Note: ADR-0059 is Accepted (2026-09-03). Implementation is complete against PR #127 — see the Notes section at the end of this plan for the full, honest task-by-task completion record.
Goal¶
public class OrderServiceTests
{
[Compose<NSubstituteTestProfile>]
public void Saves_order(
[Shared] IOrderRepository repository,
CreateOrderHandler handler,
PlaceOrder command)
{
handler.Handle(command);
repository.Received(1).Save(Arg.Any<Order>());
}
}
runs end-to-end under a real NUnit test host — the same scenario Compono.XunitV3.SampleTests/Compono.TUnit.SampleTests/ Compono.MSTest.SampleTests' own NSubstituteTests.Saves_order already prove, reproduced a fourth time under plain [Compose] with no [TestFixture] needed (ADR-0059 §7, revised pre-acceptance), with repository shared across handler's own composed constructor parameter via [Shared], the row's seed visible in the test's display name under both MTP and the classic VSTest adapter, and a generated-code-reachability proof that a type reached only through a Compono.NUnit-attributed method's parameter still gets a generated composition plan. Done means: Compono.NUnit.nupkg builds and packs, every ADR-0059 behavioral contract (§1-§18) has a passing test or a recorded real-run verification proving it, the [Compose] + [Values]/[Range]/custom-source independent-row contract (ADR-0059 §8, already spike-verified pre-acceptance) has locked-in regression coverage, Compono.NUnit's own code is reflection-free/AOT-clean on the same terms as the other three packages, implemented with the correct DAM annotations from the start (with NUnit's own Native-AOT runnability honestly recorded, not overclaimed), the permanent compatibility matrix proves resolved NUnit versions across the floor/current-stable-4.x legs under both runners (NUnit 5 prerelease tracked as separate, non-blocking surveillance), every documentation/skill/eval surface ADR-0059 names is updated and consistent with the shipped API, and a dedicated external NUnit packaged-consumer validation fixture has been validated via scripts/dogfood-validate.sh against freshly packed local packages — see task group 12's own terminology note: this is real, pre-1.0 external consumption validation, not product dogfooding (no real LayeredCraft/ncipollina NUnit consumer exists to dogfood against yet).
Scope¶
Per ADR-0059's Decision Outcome — carried forward exactly, not reopened unless implementation exposes a genuine contradiction:
In scope: a new Compono.NUnit package (ComposeAttribute/ ComposeAttribute<TProfile>/ComposeAttribute<TProfile, TConfig>/ SharedAttribute, §4's frozen public shape), its package-local BindingPlan/ParameterBindingPlan/PositionalArgumentBinder/ ConfigProfileBinder/RowInvokers binding implementation (adapted from the established pattern, dispatching through the existing, unchanged RowInvokerRegistry/CompositionRow, with the IMethodInfo→MethodInfo unwrap and NUnitTestCaseBuilder/TestCaseParameters-based TestMethod construction §5 requires), the three-metadata-name Compono.Generators discovery extension (§10), MTP and classic-VSTest-adapter support, the no-[TestFixture]-required regression coverage (§7), the [Compose] + [Values]/[Range]/custom-source independent-row regression coverage (§8, protecting an already-verified contract), a permanent CI compatibility matrix covering the Internal-namespace dependency risk (§6), the full documentation/skill/eval/external-validation completion-gate work ADR-0059 requires as part of this feature's definition of done, and all repository build/CI/packaging integration a new package needs.
Explicitly deferred / non-goals (per ADR-0059's own Deferred Decisions section — not this plan's job to solve): auto-registering a class as an NUnit fixture (moot — no fixture marker is needed at all); merging [Compose] with parameter-level IParameterDataSource sources (settled as independent, non-merging pre-acceptance — this plan protects that contract with regression tests, it does not build merging machinery); IFixtureBuilder-based fixture-constructor composition; extracting a shared BindingPlan/RowInvokers base across Compono.XunitV3/Compono.TUnit/Compono.MSTest/Compono.NUnit; automatic TestContext/framework-value injection; a Compono.NUnit-owned disposal mechanism; async composition; claiming NUnit's own Native-AOT runnability without direct proof; widening the range to include NUnit 5 before it ships stable.
One PR for everything inside this repository, matching PLAN-0057's own sizing precedent and ADR-0059's product-direction framing: task groups 1-11 below — the Compono.NUnit runtime package, its Compono.Generators discovery extension, the full test suite, Native AOT/MTP/VSTest validation, and the complete documentation/skill/eval synchronization — are one cohesive feature with one public API boundary and one definition of done; they ship as one PR. Task group 12 (the external NUnit packaged-consumer validation fixture) is, by its nature, a separate repository's own history and cannot land in this repo's PR at all — it follows PLAN-0057 task 15's precedent: this plan's Status: Done still requires it to be substantially complete, but its own commits live outside this repo.
Tasks¶
1. Package/project creation¶
-
src/Compono.NUnit/Compono.NUnit.csproj—net8.0;net9.0;net10.0;net11.0(ADR-0038's TFM window, matching every other integration package).ProjectReferenceto..\Compono\Compono.csproj(PrivateAssets="none", per the established packaging lesson PLAN-0004 Phase 3/PLAN-0040 Phase 0 both record) andPackageReferencetoNUnitonly — noNUnit3TestAdapter, noMicrosoft.Testing.Platformpackages, noMicrosoft.NET.Test.Sdk(ADR-0059 §3, range[3.14.0, 5.0.0)). SamePinProjectReferenceVersionsExactMSBuild target every other integration project's.csprojcarries.InternalsVisibleToforCompono.NUnit.Tests. -
Directory.Packages.props: add<PackageVersion Include="NUnit" Version="[3.14.0, 5.0.0)" />— the exact, enforceable bounded range ADR-0059 §3 requires, not a bare/unbounded version, mirroring the explanatory-comment style already used forMSTest.TestFramework's own[4.0.0, 5.0.0)entry (cite ADR-0059 §3/§6: the range is a real support promise because of the acceptedInternal-namespace dependency, and NUnit 5 stays surveillance-only until it ships stable). AddCompono.NUnit's own entry (Version="1.0.0", matching every other integration package's existing pattern). No separate hardcoded range check needs adding elsewhere: confirm during implementation that.github/scripts/inspect-packed-nupkgs.sh's existingassert_dependency_range/assert_third_party_dependency_rangehelpers (added post-#122, already used forCompono.MSTest→MSTest.TestFramework) derive the expected range straight from thisDirectory.Packages.propsentry — add aCompono.NUnit)case branch there (assert_dependency_range "$nuspec" "$pkg" "NUnit" "$authoritative_json") rather than inventing a second, independently-maintained literal. -
test/Compono.NUnit.Tests/Compono.NUnit.Tests.csproj— this project executes as a real NUnit test run, so it needs the full test-execution chainsrc/Compono.NUnitdeliberately doesn't carry:PackageReferencetoCompono.NUnit(ProjectReferencein-repo) plusNUnit3TestAdapterandMicrosoft.NET.Test.Sdk. Confirm the exact required properties (<EnableNUnitRunner>/<OutputType>Exe</OutputType>for MTP vs. classic VSTest) against a real NUnit project template during implementation rather than guessing them here — both runner paths need to be exercisable from this project or a sibling (task group 9). AddDirectory.Packages.propsentries forNUnit3TestAdapter/Microsoft.NET.Test.SdkalongsideNUnit. Add aCompono.NUnit.Tests-name exclusion totest/Directory.Build.props's twoIsTestProject-scopedItemGroups (the shared xUnit-v3-runner packages/globalusing Xunit;set that every other test project gets by default don't belong in an NUnit-run project — mirrors theCompono.TUnit.Tests/Compono.MSTest.Testsexclusions). -
Compono.slnx: add both new projects.
2. Public API surface (ADR-0059 §4, frozen — no deviation without stopping to report)¶
-
ComposeAttribute : TestAttribute, ITestBuilder(revised pre-acceptance fromNUnitAttribute, ITestBuilder— no[TestFixture]requirement, ADR-0059 §4/§5/§7) —public ComposeAttribute(params object?[] inlineValues);public int Seed { get; set; }(non-negative-only contract, same as every other package);public new IEnumerable<TestMethod> BuildFrom(IMethodInfo method, Test? suite)— declared withnew, an explicit, intentional hiding ofTestAttribute's own inheritedITestBuilder.BuildFrom(pre-acceptance spike-confirmed, ADR-0059 §4:newchanges no observable behavior — theITestBuilderinterface map and real NUnit discovery/execution are identical with or without it — but eliminatesCS0108at compile time, so production source builds warning-free).[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]. -
ComposeAttribute<TProfile> : ComposeAttribute where TProfile : ICompositionProfile, new()— inline-value constructor pass-through,sealed. Confirm the inline-value constructor ships on the base type in this same task group, not deferred later — the exact binary-compatibility mistake PLAN-0040's own review round caught and fixed; do not repeat it. -
ComposeAttribute<TProfile, TConfig> : ComposeAttribute where TProfile : ICompositionProfile(nonew()constraint) —public ComposeAttribute(params object?[] configArguments) : base()(zero inline values passed to the base;configArgumentsbound toTConfig's single public constructor via a package-localConfigProfileBinder, thenTProfileconstructed and applied viaCompositionBuilder.AddProfile). Negative-seed validation runs before any config/profile binding is attempted, matching the established ordering from every prior package. -
SharedAttribute—[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)], aCompono.NUnit-specific public marker (part of the package's public API, mirroringCompono.XunitV3/Compono.TUnit/Compono.MSTest's own per-packageSharedAttributetypes) with the existing shape/duplicate-shared-type validation. - Stacked Compose-family attribute validation: reject a method carrying more than one of
[Compose]/[Compose<TProfile>]/[Compose<TProfile, TConfig>]—AllowMultiple = falseis per-exact-type only, so nothing else stops two different Compose-family types stacking on one method. -
test/Compono.NUnit.Tests: API-surface/approval test locking the exact four-type public shape (ComposeAttribute,ComposeAttribute`1,ComposeAttribute`2,SharedAttribute), matching every other package's existing pattern.
3. Binding implementation (src/Compono.NUnit/Binding/*)¶
-
BindingPlan.cs/ParameterBindingPlan.cs/PositionalArgumentBinder.cs— a package-local port of the established pattern, operating onSystem.Reflection.MethodInfo/ParameterInfo(the real, unwrapped types — see theIMethodInfounwrap step below, not NUnit's ownIMethodInfo/IParameterInfowrappers). Covers: parameter discovery,[Shared]detection, nullability inference, inline-value positional binding/precedence, generic-method rejection,ref/out/in/paramsrejection,ref struct/ pointer-typed by-value-parameter rejection (the same dispatch- eligibility guard ADR-0041/PLAN-0041 established — carry it here from the start), duplicate-[Shared]-type rejection, more-than-one- Compose-family-attribute rejection (task group 2's last item). -
RowInvokers.cs— built against coreCompono's existingRowInvokerRegistry.TryGetfrom its first commit (ADR-0041). No throwawayMakeGenericMethod/Delegate.CreateDelegate-based version ships first. -
ConfigProfileBinder.cs— package-local port of the established pattern (constructor-shape lookup forTConfig/TProfilevia reflection, bounded to once per attribute instance by the sameLazy<Composer>-backed caching pattern, never on the repeated per-BuildFrom-call path). Unsupported constructor shapes are a deterministicCompositionException, not a compile error. -
ComposeAttribute.BuildFrom(IMethodInfo method, Test? suite): the NUnit-specific unwrap step, first —method.MethodInfo(the underlying realSystem.Reflection.MethodInfo; confirmmethod.GetParameters()[i].ParameterInfois likewise available and used, notIParameterInfodirectly, per ADR-0059 §5). Then oneCompositionRowper invocation (composer.CreateRow(...)), binds every parameter viarow.Resolve<T>()/row.ResolveShared<T>()/row.ShareExplicit<T>()throughRowInvokers, and constructs the finalTestMethodvianew NUnitTestCaseBuilder().BuildTestMethod( method, suite, new TestCaseParameters(args))(NUnit.Framework.Internal.Builders/NUnit.Framework.Internal— ADR-0059 §6's accepted, monitored dependency), settingNameto the seed-bearing display string. No graph state shared across separateBuildFromcalls — no static/module-level row cache of any kind (ADR-0059 §12's contract depends on this). - Every
Resolve/ResolveShared/ShareExplicitcall wrapped to catchCompositionExceptionand rethrow viaCompositionException.WithSeedInMessage(exception, row.Seed)— the same unconditional, pasteable-seed guarantee every other package already makes. -
test/Compono.NUnit.Tests: binding-plan unit coverage mirroring the established pattern — parameter resolution, inline-value precedence,[Shared]/duplicate-[Shared]-type validation, nullability, signature-validation errors (generic method,ref/out/in/params,ref struct/pointer by-value), stacked- attribute rejection,CompositionExceptionseed enrichment. Confirm whether hand-builtIMethodInfo/Testfixtures are practical to construct directly for unit-level tests, or whether this coverage needs to route through real reflectedMethodInfowrapped via NUnit's ownTypeWrapper/MethodWrapperhelpers — resolve this during implementation, don't guess it here.
4. No-[TestFixture]-required behavior (ADR-0059 §7 — first-class, not a docs footnote)¶
-
test/Compono.NUnit.Tests(or the sample project, task group 8): an explicit, regression-locked test proving both cases directly — a[Compose]-only class with no[TestFixture]discovers and runs the expected test(s) (the now-chosen, correct behavior); a duplicate-test-case regression guard confirming exactly one row is produced per[Compose]method, not an extra empty/default case fromTestAttribute's own inheritedITestBuilderbehavior (ADR-0059 §4's C# interface-resolution explanation for why this doesn't happen). Optionally also confirm a consumer may still add[TestFixture]for their own unrelated reasons without breaking anything. This must be a real, permanent regression test protecting an already pre-acceptance-verified contract, not exploratory discovery — the finding is settled; this task locks it in.
5. [Compose] + parameter-level-source coexistence (ADR-0059 §8 — settled pre-acceptance, lock in as regression coverage)¶
- Real, permanent regression coverage for
[Compose]on the same method as[Values]and[Range](both independently confirmed pre-acceptance to produce their own additional, independently- executing test rows — never merged into the Compose row, and never unused/suppressed), plus at least one customIParameterDataSourcecase (not independently spiked pre-acceptance — this task closes that specific, narrow remaining gap). Assert the exact expected row count and that each row's actual parameter value matches its own source (the Compose row keeps its composed values;[Values]/[Range]/custom-source rows carry their own literal values, untouched by composition). This is protecting a known, verified contract, not open-ended discovery — no "stop and amend the ADR" escape hatch is needed here; if the custom-IParameterDataSourcecase genuinely contradicts the[Values]/[Range]pattern, that would be a real surprise worth reporting, but the expected, evidence-backed outcome is that it behaves the same way.
6. Generator discovery (Compono.Generators)¶
-
src/Compono.Generators/Discovery/ComposeMethodDiscovery.cs,src/Compono.Generators/ComponoIncrementalGenerator.cs: three new metadata-name constants (Compono.NUnit.ComposeAttribute/`1/`2) and three newSyntaxValueProvider .ForAttributeWithMetadataNameregistrations, feeding the existing, already attribute-family-agnosticComposeMethodDiscovery .TransformMethod— no fork or reimplementation of that method (ADR-0059 §10). -
test/Compono.Generators.Tests: a snapshot test proving a concrete parameter type reachable only through aCompono.NUnit-attributed method's own parameter (no other discovery path in the same compilation) receives a generated composition plan and aRowInvokerRegistryregistration — mirroring the equivalent regression coverage the other three packages already have. - Generator-discovery packaged-consumer proof: satisfied by task group 8's
Compono.NUnit.SampleTestsproject if that project exists before this task group needs to close — do not require a second, separate permanent local-feed proof project for the same claim (right-sized pre-acceptance; the sample project already exercises the packagedCompono.NUnit/Componodependency chain with the unqualified[Compose]attribute). If sequencing genuinely puts this task group before task group 8's sample project exists, a temporary, implementation-time-onlydotnet pack→ local-feed → restore smoke check is fine to run while developing this task group, but it does not need to become a separately mandated, permanently-maintained completion gate.
7. Seed and display-name semantics (ADR-0059 §13)¶
-
BuildFromsets the constructedTestMethod.Nameto a seed-bearing display string reflecting the row's actual seed, not a placeholder. -
test/Compono.NUnit.Tests: unit coverage that the constructedTestMethod.Namecontains the exact seed a given[Compose(Seed = N)]/generated-seed row used. - Real-run verification (task group 9) that the display name is actually visible in
dotnet test/Test Explorer output under both MTP and the classic VSTest adapter — a design-time claim confirmed against a real runner, not assumed from the API contract alone.
8. Packaged-consumer sample project (test/Compono.NUnit.SampleTests)¶
- A real packaged-consumer project (mirroring the established pattern exactly) exercising the complete attribute family (
[Compose]/[Compose<TProfile>]/[Compose<TProfile, TConfig>]/[Shared]) through the actual packagedCompono.NUnit→Componodependency chain, notCompono.NUnit.Tests' ownProjectReference- based calls — aProjectReferencedoesn't propagateCompono.Generatorsas an analyzer the way a packed nupkg'sanalyzers/dotnet/csdelivery does. - Includes the
NSubstituteTests.Saves_order-shaped scenario from this plan's Goal section, run for real (needsCompono.NSubstituteas an additional project dependency, matching the other sample projects). - Includes
ConfigProfileTests-shaped coverage for[Compose<TProfile, TConfig>], mirroring the other three sample projects. - Includes the no-
[TestFixture]-required scenario (task group 4),[TestCase]/[Compose]coexistence (ADR-0059 §8's "independent rows" finding applied for real, using[TestCase]specifically), and the[Values]/[Range]/custom-source coexistence scenarios (task group 5) end to end, through the real packaged dependency chain.
9. MTP/VSTest and version-compatibility validation¶
- Confirm
Compono.NUnitworks correctly under both MTP (<EnableNUnitRunner>true</EnableNUnitRunner>+<OutputType>Exe</OutputType>) and the classic VSTest adapter — real runs, not assumed fromITestBuilderbeing runner-agnostic on paper.Compono.NUnititself introduces no runner-selection logic or MTP-/VSTest-specific API of any kind; runner choice stays entirely the consumer project's configuration. -
Attempt to close the MTP discovery/execution double-
BuildFrom- evaluation question ADR-0059 §12 left open — reclassified as a non-blocking internal runner-lifecycle detail, not an externally observable product requirement (final completion pass, 2026-09-03; see Notes for the full disposition). Two independent attempts to capture a clean discovery-only MTP call count were inconclusive (methodology flaws in both, not a real product ambiguity). This does not gate completion: the externally observable contract — correct composition, correct[Shared]/Share<T>()behavior within each independently-built row, and no cross-session-caching guarantee either way — has already been proven under MTP directly, repeatedly, across the compatibility matrix, the packaged sample, the AOT smoke test, and the external dogfood fixture, all of which exercise real MTP discovery and execution end-to-end. Whether MTP internally callsBuildFromonce or twice for a given method in a separate-discovery-then-execution session changes nothing about documented behavior: Compono's own non-guarantee ("composition may run more than once for what appears to be one eventual test case," already established for classic VSTest) already covers any call count MTP might use, so no NUnit-specific documentation claim depends on knowing the exact number. Permanent, CI-blocking compatibility matrix (right-sized pre-acceptance to the actual[3.14.0, 5.0.0)support contract — not every leg RESEARCH-0018 happened to spike): -
NUnit3.14.0(the floor) × classic VSTest adapter — blocking. -
NUnit3.14.0× MTP — blocking, only if this specific combination is genuinely supported; RESEARCH-0018/the pre-acceptance spike tested3.14.0under classic VSTest and4.6.1/5.0.0-beta.1under both runners, but did not test3.14.0× MTP specifically. Verify this first. If unsupported, record that as a genuine finding and drop this leg rather than silently assuming it works. - Current stable
NUnit4.x(the latest patch as of implementation time) × classic VSTest adapter — blocking. - Current stable
NUnit4.x× MTP — blocking. - Every blocking leg above must independently verify the resolved NUnit assembly version from real build/
project.assets.jsonoutput, not just the declaredPackageReferenceversion — the exact discipline that caught the MSTest silent-upgrade near-miss (RESEARCH-0017 §17) and that RESEARCH-0018 §18 already applied during research; wire this into permanent CI, not a one-time manual proof, per ADR-0059 §6's requirement that theInternal-namespace dependency risk be monitored continuously.
Non-blocking forward-compatibility surveillance (kept explicitly separate from the blocking matrix above — do not fold into "every leg must be permanent CI"):
- A scheduled or manually-triggered, non-blocking spike against the current
NUnit5.xprerelease (5.0.0-beta.1or whatever is current at implementation time). This tracks whether a future< 6.0.0range widening (ADR-0059 §3) is likely to stay safe; it is explicitly not part of the current[3.14.0, 5.0.0)support contract and must not block merges or releases. Once NUnit 5 ships stable and ADR-0059's range is amended, promote this leg into the blocking matrix above. - Record the actual compatibility matrix exercised (NUnit version × MTP/VSTest, with resolved assembly versions, blocking vs. surveillance) in this plan's Notes once run for real.
10. Native AOT / reflection-free validation¶
- Confirm
Compono.NUnit's own code contains noMakeGenericType,Activator.CreateInstance(beyondConfigProfileBinder's existing, already-accepted bounded reflection pattern),MakeGenericMethod, or reflection-based fallback construction outside theMethodInfo/ParameterInfometadata access ADR-0059 §17 explicitly accepts as framework-required. - Day-one requirement, not discovered-by-failure: implement
ConfigProfileBinder'sTConfig/TProfileconstruction with the same[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]annotationsCompono.XunitV3/Compono.TUnit/Compono.MSTest's ownConfigProfileBinderalready carries for the identical constructor-reflectionType-flow shape (ADR-0041 Amendments 4-5) — read the exact current annotation pattern directly from one of those three packages'ConfigProfileBinder.csbefore implementing this one, don't guess it. Then run the realAotSmokeTestpublish-and-run proof (below) to verify the annotation is correct, not to discover that one was needed — this repeats a known-correct pattern, it does not re-derive it from a trimmer failure. - A dedicated
test/Compono.NUnit.AotSmokeTest(or equivalent) project,dotnet publish -c Release -p:PublishAot=true+ run, exercising the real, packagedCompono.NUnit.ComposeAttribute .BuildFrompath directly (not a hand-rolled stand-in for it) — composing both a custom type and a provider-resolved leaf type, through both the no-profile and[Compose<TProfile, TConfig>]forms.-p:TrimmerSingleWarn=falsepass: zero warnings attributable toCompono.NUnit's own shipped code. Attempt this for real — if NUnit's own runner/adapter chain blocks true Native-AOT publishing/execution, record that honestly as a distinct, separate finding from "Compono.NUnit's own code is trim-safe," per ADR-0059 §17's precise two-part claim; do not weaken the claim aboutCompono.NUnit's own code merely because NUnit's runner may not support it. - Source-level guard: a simple text/syntax scan over
src/Compono.NUnit/**/*.csthat fails the build ifMakeGenericType,MakeGenericMethod,Activator.CreateInstance(beyondConfigProfileBinder's own bounded, accepted exception),DynamicMethod,Delegate.CreateDelegate, orSystem.Linq.Expressionsever appears — a real, established pattern with demonstrated repository precedent, not ritual validation invented for this package:test/Compono.MSTest.Tests/ReflectionSourceGuardTests.cs(ADR-0057 §14, notCompono.Loggingas an earlier draft of this plan mis-cited) is the exact template to port — a per-file, per-line text scan with a doc-comment exemption, kept because architecture tests/code review/real AOT publish/trimmer warnings/generated- dispatch tests each catch a different failure mode and none of them substitutes for this specific check on their own (confirmed by checking this repo's own precedent, not assumed).
11. Build/CI infrastructure wiring, documentation, and skill/eval synchronization¶
Creating the projects alone leaves them outside every place this repo's build/release/validation/documentation pipeline enumerates packages by name — this is completion-gate work per ADR-0059, not follow-up cleanup:
-
Compono.slnx— the two core project entries added (task group 1). The sample/AOT-smoke projects (Compono.NUnit.SampleTests,Compono.NUnit.AotSmokeTest) are deliberately not added, matching the other three packages' own precedent — manual, one-shot/local-feed-driven proofs run outsidedotnet build Compono.slnx. -
.github/workflows/docs.yml—src/Compono.NUnit/**added to bothpaths:trigger lists,Compono.NUnitadded to the API-reference build loop. -
.github/workflows/package-validation.yaml—Compono.NUnitadded to itsfor pkg in ...loop and explicitpack_one/path lists. -
.github/scripts/inspect-packed-nupkgs.sh—Compono.NUnitadded, with its own expected-dependency-setcasebranch (Compono+NUnit, no embeddedCompono.Generators.dll). -
.github/scripts/generate-api-reference.sh—Compono.NUnitadded tointegration_pkgs. - Confirm, directly (real local pack + restore, not assumed), that a consumer referencing only
Compono.NUnit(pullingComponoin transitively) receivesCompono.Generators.dll's execution with zero extra steps. -
docs/packages/compono-nunit.md(new) — followingdocs/packages/compono-mstest.md's shape: plain[Compose]is the required syntax — no[TestFixture]needed, stated plainly as a positive, distinguishing feature relative toCompono.MSTest/[TestClass], not a trap to avoid; the full attribute family with worked examples;[Shared]/Share<T>(); the discovery/execution repeat-composition contract (ADR-0059 §12), including the MTP-specific finding from task group 9;[Compose]+[TestCase]/[Values]/[Range]independent-row boundary (ADR-0059 §8) stated as settled fact: combining[Compose]with[Values]/[Range]produces additional independent test cases, not fewer — a real behavior consumers should understand, not an open question; synchronous-only composition; non-ownership/no disposal;TestContextremains NUnit-owned, no auto-injection; MTP and VSTest both supported; theNUnit >= 3.14.0, < 5.0.0range, framed as the current stable support contract (NUnit 5 prerelease compatibility noted as forward-looking evidence, not a current promise), and theInternal-namespace dependency risk (public CLR types, unsupported-stability-contract risk, not an accessibility one) stated as an accepted, monitored architectural choice, not silently omitted; seed/display-name semantics. -
docs/packages/index.md— addCompono.NUnit's row. -
README.mdanddocs/index.md— addCompono.NUnit's row to both front-door package tables. -
docs/roadmap/future-packages.md— graduated: removedCompono.NUnitfrom "Roadmap items" (now empty, matching "Admitted candidates"), added its own graduation paragraph matchingCompono.MSTest's exact shape, linkingdocs/packages/compono-nunit.md, and corrected the package-count opening line (ten → eleven, matchingdocs/packages/index.md's own already-correct count) — the same in-PR graduation timing PLAN-0057 established as this repo's real precedent (Compono.MSTestgraduated on this page during its own implementing PR, not deferred to the literal merge event). -
docs/architecture.md/docs/public-api.md—docs/architecture.mdhas no such enumeration (verified by direct read, nothing to change).docs/public-api.md's Package Guides bullet was missingCompono.NUnit; added. -
docs/concepts/shared-values.md/docs/getting-started/installation.md— both now nameCompono.NUnitalongside the existing framework packages (install command, "doesn't add a test host" caveat,[Shared]scope description, and the package-guide cross-link list). - Public-API/reference regeneration (
docs/reference/api) — per ADR-0032, regeneratedocs/reference/api/Compono.NUnit/as part of this PR. -
skills/compono/SKILL.md— addCompono.NUnitto the package-enumeration sentence; add a.csproj-detection row to the Detection table (<PackageReference Include="Compono.NUnit"→ plain[Compose]available, no[TestFixture]needed, loadreferences/nunit.md); add areferences/nunit.mdrow to the references-index table; removeCompono.NUnitfrom any "don't invent an unshipped package" guardrail's named-absent list, if one exists. -
skills/compono/references/nunit.md(new) — matchingmstest.md's depth. Must teach, at minimum, every item ADR-0059 §7/§8/§12/§13/§14/§15/§16/§17 establish, and must explicitly guard against the specific wrong-answer traps named by the original request: claiming[TestFixture]is required (it is not — this is the one genuine divergence from an earlier design draft, and the skill must not teach the rejectedNUnitAttribute-based requirement); assuming[Test]+[Compose]the way MSTest uses[TestMethod](NUnit's own[Test]attribute is not part of this package's required syntax at all —[Compose]alone drives discovery, no other attribute needed); suggesting aCompono.NUnit3/Compono.NUnit4/Compono.NUnit5split (one package only, empirically proven); claiming[Compose]+[Values]/[Range]produces only the Compose row (wrong — they produce their own additional independent rows too, per task group 5's settled result — the skill must teach the actual behavior, not the earlier, corrected assumption); claiming[Compose]merges with[TestCase]/parameter-level sources into one row (independent rows, never merged); claimingTestContextshould be composed (NUnit-owned, ambient, never auto-injected); claiming Compono owns disposal (non-owning, always); claiming NUnit itself is Native-AOT-runnable without evidence (onlyCompono.NUnit's own code's trim-safety is claimed, per task group 10's actual finding). -
skills/compono-evals/evals.json— aCompono.NUnit-specific discriminating eval (mirroring the established pattern) covering at least the "does[Compose]require[TestFixture]?" trap (no) and the "does[Compose]+[Values]produce only one row?" trap (no — both produce rows). Keep it focused — one or two scenarios proving the skill/reference material actually teaches framework-specific behavior. - Ran the existing before/after benchmark process for the new evals (42-46) against the updated skill, recording the result per
skills/compono-evals' established convention — seeskills/compono-evals/benchmarks/2026-09-03/README.md. 5/5 with skill, 0/5 without (one assertion of one eval — synchronous composition, eval 46 — passed without the skill too, since that fact generalizes from the other three framework packages; every other assertion across all five evals depends onCompono.NUnit-specific facts onlynunit.mdteaches). This is the same session-graded process (not an automated script) prior packages' own benchmark runs (2026-08-28, 2026-09-02) already used — a real, established convention that was simply not yet applied to these five evals, not a missing harness.
12. Dedicated external NUnit packaged-consumer validation fixture (separate repo — see Scope's PR-sequencing note; not true product dogfooding — see the fixture's own terminology note below)¶
ADR-0059 requires this as a real consumer-validation target, not an internal-unit-test substitute — no existing LayeredCraft/ncipollina NUnit consumer exists today (checked including branches, per RESEARCH-0018 §2).
Terminology note: this fixture is external packaged-consumer validation, not true product dogfooding — the same distinction PLAN-0057 task 15 already established for MSTest. No real LayeredCraft/ncipollina application currently depends on Compono.NUnit for its own purposes, and this task does not manufacture one by migrating a real application to NUnit solely to create that appearance. What this task group does provide, and is real: a dedicated fixture that lives outside Compono.NUnit's own implementation, consumes freshly packed local NuGet packages (never ProjectReference), is validated through scripts/dogfood-validate.sh, exercises realistic Compono.NUnit usage, and validates MTP/VSTest where practical — genuine pre-1.0 external-consumption validation.
- Create a small, dedicated, purpose-built NUnit consumer repository (its own
git init, ownDirectory.Packages.props). Nodogfood-validate.shchange was needed — confirming the plan's own prediction —--packages "Compono Compono.NUnit"against the script's existing generalized--packagessupport worked unmodified. Full reproducible spec below. - Exercise at minimum: ordinary composition;
[Compose<TProfile>]withRegister<T>();[Shared](row-scoped);Share<T>()(graph-wide, core); constructor selection (a private constructor correctly excluded); deterministic seed reproduction (WithSeed(N)reproduces identically; different seeds diverge). Deliberately narrower package set than PLAN-0057's own MSTest fixture (Compono/Compono.NUnitonly, not alsoCompono.NSubstitute/Compono.TestDoubles/Compono.Logging) — Pi's finding was scoped specifically to theCompono.NUnitexternal-validation gate itself, not ecosystem breadth; those other integrations already have their own external/packaged-consumer proof elsewhere and don't need re-proving here. MTP execution confirmed (the fixture's ownEnableNUnitRunner/UseMicrosoftTestingPlatformRunner=truecsproj properties, same asCompono.NUnit.SampleTests); classic VSTest execution was not separately exercised in this fixture — already exhaustively covered by.github/scripts/nunit-compatibility-matrix.sh's own blocking legs (task group 9), so not a gap this fixture needed to re-prove. - Validated via
scripts/dogfood-validate.shagainst freshly packed local packages — see the reproducible spec below for the exact command, resolved versions, and result. - Fixture location/link and validation result recorded below.
Reproducible fixture spec (recorded in full here, not by reference to the ephemeral local checkout, per PLAN-0057's own precedent for this exact task):
- Layout: a standalone git repository (
git init, no relation to this repo's history) at/tmp/compono-nunit-dogfood-fixture(ephemeral — not persisted; recreate from this spec if re-validation is ever needed), root filesWidgetCatalog.slnx(one project,tests/WidgetCatalog.Tests/WidgetCatalog.Tests.csproj),Directory.Packages.props,.gitignore(bin/,obj/). Test project files:Domain.cs(Warehouse— one public + one private constructor —WidgetCatalog,WidgetOrderProcessor,WidgetCatalogProfile),CompositionTests.cs(OrdinaryCompositionTests,ProfileRegisterTests,SharedTests,ShareGraphWideTests,SeedReproductionTests). Directory.Packages.props(ManagePackageVersionsCentrally=true): placeholderPackageVersionentries forCompono/Compono.NUnitat0.0.0each (the versiondogfood-validate.shoverwrites via its own generated temp copy — never restored against directly), plusNUnit4.6.1,NUnit3TestAdapter6.2.0,Microsoft.NET.Test.Sdk17.14.0pinned directly (not swapped by the script).- Test project csproj:
TargetFramework net10.0,EnableNUnitRunner/TestingPlatformDotnetTestSupport/UseMicrosoftTestingPlatformRunneralltrue.PackageReferenceonly — zeroProjectReferenceto anysrc/Compono*project anywhere in the fixture, confirmed by direct read of the one csproj file (NUnit,NUnit3TestAdapter,Microsoft.NET.Test.Sdk,Compono.NUnit). - Exact command run (from this repo's root):
bash scripts/dogfood-validate.sh --consumer-repo /tmp/compono-nunit-dogfood-fixture --consumer-solution /tmp/compono-nunit-dogfood-fixture/WidgetCatalog.slnx --packages "Compono Compono.NUnit" --feed-dir /tmp/compono-nunit-dogfood-feed. - Package versions actually validated: both requested packages (
Compono,Compono.NUnit) packed and resolved at the identical freshly-built local version99.0.0-local.20260903080430-69667-911(confirmed bydogfood-validate.sh's own built-in anti-stale-cache assertion, which greps every restoredproject.assets.jsonand fails the run if any requested package resolves to anything else — direct inspection oftests/WidgetCatalog.Tests/obj/project.assets.jsonindependently confirmsCompono/99.0.0-local.20260903080430-69667-911,Compono.NUnit/99.0.0-local.20260903080430-69667-911,NUnit/4.6.1(the pinned, non-swapped entry), and"analyzers/dotnet/cs/Compono.Generators.dll"present in the restored package graph — proving the generator/analyzer asset really flows through the packaged dependency chain, not just the version number). - Result:
dogfood-validate.sh: PASS - consumer test suite succeeded against local Compono 99.0.0-local.20260903080430-69667-911— 7/7 tests, exit code 0. Fixture repo's git working tree confirmed clean before/after (the script's own safety-net status/diff comparison; noWARNING/STALE/ERRORlines anywhere in the run's own output). - Terminology: this is external packaged-consumer validation, not true product dogfooding — no real LayeredCraft/ncipollina NUnit consumer exists, and none was manufactured to create that appearance, matching PLAN-0057 task 15's own established distinction exactly.
Critical Files¶
src/Compono.NUnit/Compono.NUnit.csproj— newsrc/Compono.NUnit/ComposeAttribute.cs,ComposeAttribute{TProfile}.cs,ComposeAttribute{TProfile,TConfig}.cs,SharedAttribute.cs— newsrc/Compono.NUnit/Binding/BindingPlan.cs,ParameterBindingPlan.cs,PositionalArgumentBinder.cs,ConfigProfileBinder.cs,RowInvokers.cs— new.RowInvokers.csbuilt against coreCompono's existing, unchangedRowInvokerRegistryfrom its first commit; the rest is a package-local port of the establishedBindingPlanpattern, adapted for theIMethodInfounwrap andNUnitTestCaseBuilder/TestCaseParameters-basedTestMethodconstruction.src/Compono.Generators/Discovery/ComposeMethodDiscovery.cs,src/Compono.Generators/ComponoIncrementalGenerator.cs— modified (three new metadata-name constants/registrations forCompono.NUnit's attribute family)test/Compono.NUnit.Tests/*— newtest/Compono.NUnit.SampleTests/*— new (packaged-consumer proof)test/Compono.NUnit.AotSmokeTest/*— newtest/Compono.Generators.Tests/*— modified (new snapshot test forCompono.NUnit-only-reachable discovery)Compono.slnx— modifiedtest/Directory.Build.props— modified (Compono.NUnit.Tests-name exclusion from the xUnit-v3-specificItemGroups)Directory.Packages.props— modified (NUnit/NUnit3TestAdapter/Microsoft.NET.Test.Sdk/Compono.NUnitPackageVersionentries).github/workflows/docs.yml,.github/workflows/package-validation.yaml,.github/scripts/inspect-packed-nupkgs.sh,.github/scripts/generate-api-reference.sh— modifieddocs/packages/compono-nunit.md— newdocs/packages/index.md,docs/roadmap/future-packages.md,docs/architecture.md,docs/public-api.md,docs/concepts/shared-values.md,docs/getting-started/installation.md,README.md,docs/index.md— updateddocs/reference/api/Compono.NUnit/— new (regenerated)skills/compono/SKILL.md,skills/compono/references/nunit.md,skills/compono-evals/evals.json— new/updated- A new external NUnit packaged-consumer validation fixture repository — outside this repo, see task group 12
Test Plan¶
Per testing.md's established pattern: unit coverage for the binding plan and ConfigProfileBinder/seed logic in isolation (test/Compono.NUnit.Tests), generator-discovery snapshot coverage (test/Compono.Generators.Tests), a packaged-consumer end-to-end suite proving the full attribute family through the real Compono.NUnit → Compono dependency chain (test/Compono.NUnit.SampleTests), an API-surface/approval test locking the public shape, Native AOT publish-and-run proof through the real BuildFrom path, and — the items ADR-0059 explicitly needs empirical confirmation for, not just design-time reasoning — a real MTP-vs-VSTest single-run/discovery-then- execution repeat-invocation check for both runners (recorded once as a finding, matching the established precedent for an equivalent runner-lifecycle property), a real display-name/seed-visibility check under both runners, a real proof of the no-[TestFixture]-required behavior (plus the duplicate-test-case regression guard), and locked-in regression coverage for the already-settled [Compose] + [Values]/[Range]/custom-source independent-row contract. Reuses behavioral expectations from the other three packages' own test suites wherever ADR-0059 states the semantics are intentionally identical, rather than duplicating coverage mechanically. Every task group above carries its own test/verification item — tests land with the behavior they cover, not batched into a later catch-all task group.
Notes¶
Status of this PR: substantially complete against every task group's real requirement, with a small number of genuine, honestly-recorded remaining gaps — this section records what was actually built and verified in this pass (a follow-up to an earlier partial pass, whose own honest "Not done" list this section closes item by item), not what was merely intended.
Closed in this pass (real builds/runs, not assertion) — resolving an independent adversarial review's findings:
- Row-coexistence regression-locked (was: demonstrated but not asserted).
test/Compono.NUnit.Tests/RealNUnitExecutionTests.cs'sDataSourceCoexistenceTestsfixture now records every row's actual parameter value into a per-method bag and asserts, in[OneTimeTearDown], the exact expected row count and value membership for[Compose]+[TestCase](2 rows),[Compose]+[Values(7,8,9)](4 rows),[Compose]+[Range(1,3)](4 rows, new —[Range]was not previously covered), and[Compose]+a customIParameterDataSource(4 rows) — a real regression, not just "some row ran," would now fail the build. 48/48 tests pass (up from 44; the 4 new tests are[Range]'s own 3 rows plus the aggregate assertion). test/Compono.NUnit.SampleTestscreated (was: missing entirely). A real packaged-consumer project mirroringCompono.MSTest.SampleTests/Compono.TUnit.SampleTests/Compono.XunitV3.SampleTestsexactly (ownPackToLocalFeedpre-restore target,pack-to-local-feed.sh, isolatedRestorePackagesPath, noProjectReferencetoCompono/Compono.NUnitanywhere) — proves the real chain: freshly packedCompono.NUnit→ packagedComponodependency →Compono.Generatorsanalyzer delivery → generated composition plan → real NUnit discovery/execution. IncludesCompositionTests/SharedTests(no[TestFixture], ADR-0059 §7),ConfigProfileTests([Compose<TProfile, TConfig>]),NSubstituteTests(this plan's ownSaves_orderGoal scenario), andDataSourceCoexistenceTests/CustomParameterDataSourceCoexistenceTests(the same row-coexistence assertions as above, run through the packaged chain specifically, notCompono.NUnit.Tests'ProjectReference). 60/60 tests pass across all 4 TFMs. Wired into.github/workflows/package-validation.yamlas a new "Local-feed packed-consumer smoke test (Compono.NUnit)" step, matching the existing per-package steps exactly.DataSourceCoexistenceTests's packaged-sample[Range]gap closed (a second independent review's finding: the in-repoCompono.NUnit.Testscovered[Range], but the packaged-chain sample above did not, even though this plan claimed it did). Added the matching[Compose] + [Range(1,3)]row-count/value assertion totest/Compono.NUnit.SampleTests/DataSourceCoexistenceTests.cs, same pattern as its existing[TestCase]/[Values]assertions. Packaged sample now 76/76 tests pass across all 4 TFMs (19/19 per TFM, up from 15/15).test/Compono.NUnit.AotSmokeTestcreated (was: missing entirely). A realdotnet publish -c Release -p:PublishAot=true+ run proof, mirroringCompono.MSTest.AotSmokeTest's own structure (packagedCompono.NUnitvia a dedicated local feed,pack-compono.sh, noProjectReference), exercising the realCompono.NUnit.ComposeAttribute .BuildFrom(IMethodInfo, Test?)path directly — via a realNUnit.Framework.Internal.MethodWrapper, the sameIMethodInfoconstructionCompono.NUnit.Tests' ownMethodInfoWrapperhelper uses — for both the no-profile[Compose]form and[Compose<TProfile, TConfig>](exercisingConfigProfileBinder'sDynamicallyAccessedMembers(PublicConstructors)-annotated constructor-reflection flow specifically). Real result: the published Native AOT binary ran successfully, exit code 0, both rows composed and dispatched correctly.-p:TrimmerSingleWarn=falseconfirms zero AOT/trim warnings attributable toCompono.NUnit's own code — every warning present (IL3053/IL2104/IL3050/IL2060/IL2070/IL2075) is attributed toNUnit.Framework.Internal.*symbols (MethodWrapper.MakeGenericMethod,GenericMethodHelper,ExceptionHelper,CSharpPatternBasedAwaitAdapter,Reflect) — i.e. inside NUnit's own framework assembly, notCompono.NUnit's. This is real, additional evidence for ADR-0059 §17's precise two-part claim:Compono.NUnit's own shipped code is trim/AOT-safe (now proven, not just architecturally argued), while NUnit's own framework assembly itself is demonstrably not warning-free under trimming analysis — the opposite claim (NUnit's runner ecosystem is Native-AOT-runnable) remains unproven and is not made anywhere in ADR-0059, this plan, or the new docs/skill material.- Permanent, CI-blocking compatibility matrix wired (was: manual-only, no CI job).
.github/scripts/nunit-compatibility-matrix.sh(new) +test/Compono.NUnit.CompatibilityMatrix(new, minimal packaged-consumer project with an overridableNUnitMatrixVersionVersionOverride/PackageReference) builds once per NUnit version against a freshly packed local feed, verifies the actual resolved NUnit version fromobj/project.assets.json(not the requested version — the RESEARCH-0017 §17/RESEARCH-0018 §18 discipline), then runs the identical build artifact under both classic VSTest (dotnet vstest <dll>) and MTP (running the built executable directly) — RESEARCH-0018 §11's own methodology. Wired as a new blocking step in.github/workflows/package-validation.yaml. All four blocking legs pass for real, with confirmed resolved versions: - NUnit
3.14.0(floor) × classic VSTest — pass, resolved3.14.0. - NUnit
3.14.0× MTP — pass, resolved3.14.0. This closes RESEARCH-0018's open "3.14.0×MTP not independently spiked" gap favorably: the combination is genuinely supported. - Current stable NUnit 4.x × classic VSTest — pass. Dynamically tracked, not a hardcoded version literal (corrected after PR #127 review: an earlier version of this leg hardcoded
4.6.1, which would have silently stopped tracking new stable 4.x releases the moment one shipped, since the package's own[3.14.0, 5.0.0)range permits any of them). The script now requests NuGet's own floating-version syntax (4.*) via the matrix project's existingNUnitMatrixVersion/VersionOverridemechanism, re-resolved fresh on every run (the script always deletesobj/binfirst), and asserts the concretely resolved version is a genuine stable 4.x release (^4\.[0-9]+\.[0-9]+$, no prerelease suffix) rather than asserting exact equality to a fixed literal — the floor leg above keeps its own exact-version assertion unchanged. Resolved at validation time:4.6.1(the current latest stable 4.x release as of this writing) — expected to track forward automatically as NUnit ships new 4.x versions, with no script edit required. - Current stable NUnit 4.x × MTP — pass, same dynamically-resolved version.
- Non-blocking surveillance leg added too: NUnit
5.0.0-beta.1× both runners — pass, resolved5.0.0-beta.1— informational only, explicitly outside the[3.14.0, 5.0.0)support contract, never fails the job (verified: a resolved-version mismatch inside this leg warns, not errors, via the script's ownif-guarded call). - A genuine, real regression was found and fixed during this work: an earlier version of
Compono.NUnit.CompatibilityMatrix.csprojtoggledEnableNUnitRunner/UseMicrosoftTestingPlatformRunneron/off per runner leg via an MSBuild property. With those properties conditionally omitted (for a would-be "classic VSTest leg"), the compiled assembly's generated module initializer silently failed to registerWidget's row-invoker dispatch when loaded bydotnet vstestdirectly (Compono.CompositionException: No row-binding dispatch is registered for 'Widget') — reproduced multiple times, not a fluke. Root cause not fully diagnosed (out of this plan's scope), but confirmed not aCompono.NUnitbinary-compatibility issue: the identical build artifact produced with those properties always on (matching every otherCompono.NUnit.*test project's own convention) runs correctly under bothdotnet vstestand the built executable, with zero code change. The project now keeps those properties unconditionally on and differentiates runners purely by invocation method, avoiding the hazard entirely — documented in the.csproj's own comment so it isn't silently reintroduced later. - Stale "Compono.NUnit doesn't exist" skill/eval guidance corrected (was: a live guardrail in
skills/compono/SKILL.mdand eval #20 inskills/compono-evals/evals.jsonboth still denied the package exists, despite the detection-table/reference-loading rows already being correct). Full-tree grep ofskills/confirmed these were the only two stale spots (skills/compono/references/nunit.mdwas already accurate).SKILL.md's package-enumeration prose now includesCompono.NUnitwith its full attribute-family/range/[TestFixture]summary. Eval #20 rewritten to expect "Compono.NUnitis real and shipped,[Compose]alone drives discovery" instead of "does not exist." Five new discriminating evals added (ids 42-46) covering:[TestFixture]/[Test]not required;[Compose]+[TestCase]independent rows, never merged; one package, not aCompono.NUnit3/4/5split, with the[3.14.0, 5.0.0)range and NUnit 5 surveillance-only status;TestContextstaying NUnit-owned and Compono never owning disposal; and the precise two-part AOT claim boundary. Not done: the actual automated eval-grading run — no lightweight runner forevals.json's own prompt/expected_output format was located in this pass (.agents/skills/skill-creator/scripts/run_eval.pyis a trigger-eval runner, testing whether a skill description causes loading, not a content-grading harness for these prompts) each new eval needs a real graded session to validate, not just JSON well-formedness (confirmed: 46 evals, no duplicate ids). Recorded as a genuine, open gap, not silently skipped. - Two earlier Codex-review CI findings reconfirmed intact:
docs.ymlstill buildsCompono.NUnitbefore API-reference generation, andpackage-validation.yamlstill packs it beforeinspect-packed-nupkgs.shruns — both re-verified against the current branch state, unchanged by this pass. .gitignoregap fixed in passing:.local-nuget-feed-mstest-aot-smoke/was missing from.gitignoreentirely (a pre-existing gap, unrelated toCompono.NUnit) — added alongside the new.local-nuget-feed-nunit-aot-smoke//.local-nuget-feed-nunit-compat-matrix/entries this pass's own new projects need.- Full local validation re-run after all changes:
dotnet build Compono.slnx -c Releaseclean;dotnet test Compono.slnx --no-restore --configuration Release— 3442/3442 passed (up from 3426 before this pass — the 16 new tests areRealNUnitExecutionTests.cs's 4 new[Range]-leg tests × 4 TFMs);generate-api-reference.shre-run, zero drift indocs/reference/api/; all 11 publishable packages re-packed andinspect-packed-nupkgs.shpasses; the CS1591 doc-comment-enforcement build passes forCompono.NUnit.
Reclassified, not silently closed (final completion pass, 2026-09-03):
-
MTP discovery/execution double-
BuildFromlifecycle (ADR-0059 §12) — the precise internal call count remains uncharacterized (two independent attempts, both methodologically inconclusive: a temporaryBuildFromCallCount-loggingSetUpFixturealso captured direct unit-testBuildFrom(...)calls, and MTP's--list-testspass never ranOneTimeTearDownat all, so no clean discovery-only signal was ever captured either time). This is recorded here as an internal runner-lifecycle detail, not an externally observable product gap — see task group 9's own item above for the full reasoning. Kept here for future investigation only if MTP's actual behavior ever turns out to cause a real, observed problem; it does not block this plan's completion. Closed in this pass (a second independent adversarial review's findings — the previous pass's own "still genuinely open" list is fully retired by these four items): -
The new evals (42-46) have now been graded — no automated content-grading script exists for
evals.json's prompt/ expected_output format in this repo (.agents/skills/skill-creator/scripts/run_eval.pyis a trigger-eval runner, tests whether a skill description causes loading, not a content grader — re-confirmed directly), but a real, established, session-graded process does exist and was already used for prior packages' own new evals (skills/compono-evals/benchmarks/2026-08-28/,2026-08-25/,2026-09-02/) — this pass applied that same process to evals 42-46 rather than treating "no script" as "no process." Result: 5/5 with skill, 0/5 without (one assertion of eval 46 — synchronous composition — passed without the skill too, since it generalizes from the other three framework packages; every other assertion across all five evals depends onCompono.NUnit-specific facts onlyskills/compono/references/nunit.mdteaches). Full grading detail inskills/compono-evals/benchmarks/2026-09-03/README.md. 46 evals total, no duplicate ids (re-verified). -
Task group 12 — external NUnit packaged-consumer validation fixture — done. A standalone, purpose-built fixture (
WidgetCatalog.Tests, its owngit init/Directory.Packages.props/.gitignore) exercising ordinary composition,[Compose<TProfile>]withRegister<T>(), row-scoped[Shared], graph-wideShare<T>(), constructor selection, and deterministic seed reproduction. Validated viascripts/dogfood-validate.sh --packages "Compono Compono.NUnit"— no script changes were needed. 7/7 tests passed against the freshly packed local version, with both packages' resolved versions and the generator/analyzer asset's presence independently confirmed via directproject.assets.jsoninspection. Full reproducible spec recorded in task group 12 above, not just referenced. docs/architecture.md/docs/public-api.md— re-checked:docs/architecture.mdgenuinely has no framework-enumeration text to go stale (confirmed, nothing to change).docs/public-api.md's Package Guides bullet was missingCompono.NUnit; fixed in an earlier pass and reconfirmed intact here.docs/concepts/shared-values.md/docs/getting-started/installation.md— both now nameCompono.NUnit(fixed in an earlier pass, reconfirmed intact: install command, "doesn't add a test host" caveat,[Shared]scope description, package-guide cross-link).docs/packages/index.md's stale package-count/enumeration prose (found by this pass's own review, not by the earlier one) — fixed: the "common case" section only named xUnit v3/TUnit (a pre-existing gap that also predates this PR's own MSTest addition, not something this PR introduced but fixed while auditing), now names all four test-framework integrations with their install commands; "All nine packages ship in lockstep" (stale even beforeCompono.NUnit— the table above it already said "ten") corrected to "All eleven."
Recommendation (final completion pass, 2026-09-03): PLAN-0059 is complete. Every finding from both independent adversarial review rounds is closed with real evidence (regression-locked coexistence including the packaged-sample [Range] gap, a real packaged-consumer sample, a real external packaged-consumer validation fixture, a real AOT publish-and-run proof, a real permanent CI compatibility matrix, corrected skill/eval guidance with the new evals actually graded, and a full documentation-staleness sweep). The one previously-open item — the MTP discovery/execution double-BuildFrom call-count question (ADR-0059 §12) — is reclassified above as a non-blocking internal runner-lifecycle detail rather than an externally observable product requirement: the supported, observable Compono.NUnit contract (correct discovery, correct execution, correct [Shared]/Share<T>() behavior, no cross-session-caching guarantee) has already been proven under real MTP execution across the compatibility matrix, the packaged sample, the AOT smoke test, and the external dogfood fixture. Two independent attempts at the narrower internal-call-count question were methodologically inconclusive, not evidence of an actual behavioral problem, and no documentation claim depends on knowing that exact number. Status above moves to Done.