[PLAN-0071] xUnit v3 Composed Theory Rows ([ComposeData], Issue #163)¶
Status: Done
Implements: ADR-0070 (issue #163). ADR-0070 is Accepted (2026-11-05). B1–B9 are complete and validated (Pass 4 / B9 final validation, 2026-11-10).
Goal¶
Compono.XunitV3 and Compono.XunitV3.Aot both ship [ComposeData], [ComposeData<TProfile>], and [ComposeData<TProfile, TConfig>]. Each row from a public static member (sync or async) is composed at execution time with its own profile instance, composer, seed, and [Shared] scope. Row values bind leading-positionally, and failures identify the row. JIT and Native AOT behave identically, with real Native AOT proof. Docs, cookbook, API reference, and the distributed compono skill describe the shipped API.
Scope¶
In: everything in ADR-0070's Decision Outcome. That covers the public API (both packages), the shared runtime helper (linked source), the JIT implementation, JIT generator discovery, AOT generator discovery, diagnostics, and emission (including [Shared], row values, TConfig per row, seeds, and row metadata), tests, samples, docs, the skill, and validation.
Deferred / out of scope:
- Member arguments, generic member methods (
Cases<T>(); rejected on both engines: a runtime failure on JIT,CMP0052on AOT), and per-row seed overrides. - Dynamic per-derived-class member selection for inherited test methods on AOT (see "B4 specification amendment" below: AOT resolves on the method's declaring class, as xUnit's own AOT
MemberDatadoes). - TUnit, NUnit, and MSTest equivalents.
- AOT
[Compose]inline values and[Shared]. The emission components are built reusable but not wired into[Compose]. Record the follow-up indocs/packages/compono-xunitv3-aot.md. - AOT
[Compose]seed reporting (#166). See the coordination checkpoint in B5. - Any disposal ownership (ADR-0022 Amendment 4 stands).
- Discovery-time enumeration.
-
164, #165, #166 implementation.¶
- Core
Componochanges, except one approved additive API (ADR-0070 Amendment 1): a publicCompositionException(string message, CompositionDiagnostic? diagnostic, Exception innerException)constructor so the shared helper can build the R0 row-failure wrapper while preserving the cause asInnerExceptionand the exactDiagnosticreference. Plus one approved core fix (ADR-0070 Amendment 6, 2026-11-10):CompositionBuilder.AddProfile<TProfile>()rethrows a profile constructor's original exception instead ofActivator'sTargetInvocationExceptionwrapper. It's behavior-only, with no API change, and lands in this branch (B6 Pass 1). Any further core change still stops and raises.
Release: ships in the single #161 release with #162–#166. No interim release.
Work categories (where each bead sits)¶
| Category | Beads |
|---|---|
| Public API | B2 (attributes, both packages), B1 (shared helper surface: internal on JIT, EditorBrowsable(Never) public on AOT) |
| JIT implementation | B3 |
| Native AOT implementation | B4 (discovery and diagnostics), B5 (emission) |
| Tests | Every implementation bead carries its own tests. B6 adds real-runner, sample, and Native AOT tests |
| Documentation | B7 |
| Skill (mandatory) | B8 |
| Validation | B9 (plus per-bead gates) |
Phases and PRs¶
One cohesive feature PR (product-owner decision, review round 1). The work happens on one feature branch, feat/163-composedata, which the B1 implementer creates from main after ADR-0070 is Accepted. A single PR covers JIT, AOT, tests, docs, and the mandatory skill update. No partial feature is merged to main: no JIT-only, AOT-only, or docs-less PR.
The phases below are internal checkpoints, not PR boundaries. Each bead must leave the branch building with 0 warnings and 0 errors and with all tests green, and is reviewed at its checkpoint (local commits are fine, but only when the product owner approves committing):
- Phase 1 checkpoint: shared helper, public API, JIT. B1 → B2 → B3.
- Phase 2 checkpoint: Native AOT. B4 → B5 → B6.
- Phase 3 checkpoint: docs, skill, validation. B7 → B8 → B9. The PR opens only after B9, and only with product-owner approval.
Streamlined remaining work (product owner, 2026-11-10). From B6.2 on, the remaining work runs as four passes (B6, "Streamlined remaining work"):
- Pass 1: core fix, plus B6.2/B6.3 behavioral coverage.
- Pass 2: reduced B6.4, plus B6.5 samples.
- Pass 3: B7 + B8.
- Pass 4: B9.
Each pass is one cohesive implementation, one independent review, and one commit that carries both the implementation and its plan checkpoint. There's no separate documentation-closeout commit. Commits still need product-owner approval.
This intentionally relaxes the repo's phase-per-PR default for this feature: the phases aren't independently shippable, because a JIT-only ComposeData would break the parity promise. Nothing is released until the whole #161 set is done.
Specification details¶
Shared helper (src/Shared/Compono.XunitV3/ComposeDataRows.cs)¶
One file linked into both projects, like FixtureContextExtensions.cs. It uses #if COMPONO_XUNITV3_AOT for the namespace and for accessibility: internal static class on JIT, public static class with [EditorBrowsable(EditorBrowsableState.Never)] on every member on AOT (ADR-0058). It never composes. The names below are suggestions, but the responsibilities and messages are normative.
- Collection classification (ADR-0070 item 3, RESEARCH-0038 §2.7): the awaited member result must be an
IEnumerableother thanstring, or anIAsyncEnumerable<T>(the async loop itself lives in the JIT code or the generated code, since the helper is non-generic over the collection). Anything else is D4.nullis D5. - Row normalization: take
object? row. Return the values array and an optional sourceITheoryDataRow. Classify in this order:ITheoryDataRow(GetData()), thenobject?[](as-is), thenITuple(indexer, which flattens 8+ elements).nullor any other type is a row error. No reflection. - Config extraction (
TConfigform): require element 0 to be present, non-null, andis TConfig. The values become elements 1..n. - Value validation:
T BindValue<T>(object?[] values, int index, bool parameterIsNullable, string parameterName, …). This uses the same rule and the same message text asPositionalArgumentBinder/ComposeAttribute(null-not-allowed, type mismatch). It's generic over the closed parameter type, so it's AOT-safe. Values arrive boxed (Nullable<T>asT/null). There's no widening orTypeHelperconversion. A reference parameter in an oblivious nullable context is non-nullable on both engines: the JIT rule inBindingPlan.GetNullability, and on AOT the generator mapsNullableAnnotation.Noneto non-nullable. The JIT path may keep usingPositionalArgumentBinderdirectly, but must produce identical text. - Metadata merge (single rule, ADR-0070 "Row and attribute metadata"): the input is the source row's metadata plus the attribute's metadata. Skip is grouped: the row's
(Skip, SkipType, SkipUnless, SkipWhen)ifrow.Skip != null, else the attribute's group ifattr.Skip != null, else none.Label/TestDisplayName/Timeout/Explicit/DisableParallelizationare row ?? attribute.Traitsis row ∪ attribute pairs ∪Compono.Seed. Engine-specific types sit behind#if. On JIT, conditions arestringnames andSkipTypeis passed through. On AOT, conditions areFunc<bool>(the attribute-level ones are rendered by the generator as() => global::T.ProponSkipType ?? test class). Don't route through xUnit'sConvertDataRow(JIT, which drops row conditions) orDataAttributeRegistration.CreateDataRow(AOT, which merges field by field). - Output row:
new TheoryDataRow(args)with the merged metadata. JIT'sDisableParallelization(4.x-only, and the package compiles against 3.2.2) is read from the source row and set on the output row through cachedPropertyInfos (resolved once per process, null on 3.2.2, where the step is a no-op). It's narrowly scoped to this one property inCompono.XunitV3, and approved (RESEARCH-0038 §9.2 S1). - B5 additions (ADR-0070 Amendment 4 §3, PLAN B5.5):
MemberReadFailure,NegativeAttributeSeed, andNegativeRowSeed(shared; the JIT attribute switches to them), plus the AOT-onlyResolveGenericTestClassType. They're the only helper members B5 adds. - Failure construction: wrap the cause into
CompositionException, keepingInnerException= the cause andDiagnosticcopied when the cause is aCompositionException(reuseCompositionException.WithSeedInMessagesemantics), with the formats below.
Messages (byte-identical on both engines, except that single-sentence compile-time diagnostics omit the trailing period per RS1032 - see the RS1032 note under the diagnostics table; {M} = Namespace.Type.Member, {T} = test method display name as BindingPlan.MethodDisplayName)¶
- D1, member not found:
ComposeData member '{M}' for '{T}' was not found. Expected a public static field, property, or parameterless method. - D2, not public static:
ComposeData member '{M}' for '{T}' must be public and static - D3, needs arguments:
ComposeData member '{M}' for '{T}' requires arguments, which ComposeData does not support - D4, unsupported return:
ComposeData member '{M}' for '{T}' returned unsupported type '{type}'. Expected IEnumerable, Task/ValueTask of IEnumerable, or IAsyncEnumerable of ITheoryDataRow, object?[], or ITuple rows. - D5, null result:
ComposeData member '{M}' for '{T}' returned null. - D6, no rows: let xUnit's own "No data found" behavior stand. Don't invent a Compono error.
- R0, row wrapper:
ComposeData row {i}{ [label]} from '{M}' for '{T}' failed: {cause message}.{ [label]}is[<label>]when the source row has a non-null label, otherwise empty. The text then ends with a seed line per the seed rule below. - Row causes (the
{cause message}inside R0):row is null;row has unsupported type '{type}'; expected ITheoryDataRow, object?[], or ITuple;the first row element must be a non-null '{TConfig}' profile configuration value(null, missing, or wrong type);too many values: {n} supplied, but the method has {p} parameter(s); the existing inline null and type-mismatch texts (with "Inline value" replaced by "Row value"); a pipelineCompositionExceptionmessage; or a profile constructor orConfigureexception message. - Seed rule (ADR-0070 "Effective seed"; never fabricated):
- D1–D5 (member level): no seed line.
- R0 before
CreateRow(row shape,TConfig, profile constructor,Configure,Composer.Createincluding a duplicate-seed conflict):\n\nSeed: {Seed}if attributeSeedis set, otherwise\n\nSeed: not assigned (the failure occurred before the row's composition started). - R0 from
CreateRowon:\n\nSeed: {row.Seed}. - Negative attribute
Seed: the existing negative-seed message, checked before the member is read. A negative profile seed is caught by therow.Seed < 0check right afterCreateRowand wrapped in R0. - Per-row order: normalize the row, extract
TConfig, build the profile andComposer,CreateRow, negative check, value validation,[Shared], then the rest. - Attribute
Seedplus profileWithSeedis core'sDuplicateConfigurationOptionerror (no override). Assert it in tests.
Diagnostics (AOT, CMP0051–CMP0057, added to AnalyzerReleases.Unshipped.md)¶
Numbering is final (B4 specification amendment, 2026-11-06). Every ID is Error, category Compono.Usage. Each new ID has a JIT runtime counterpart for the same condition (D1–D4/R0, or JIT's member-read/ambiguity CompositionException). {0} is the lookup type's display name plus .Member; {1} is the test method's display name (the existing AOT methodDisplayName convention: compile-time messages use Roslyn display names; only the runtime R0 text must be CLR-style, see B4.3).
| ID | Condition | Message text (one descriptor per ID; the reason varies) |
|---|---|---|
CMP0051 | member not found; member ambiguous | ComposeData member '{0}' for '{1}' was not found. Expected a public static field, property, or parameterless method. / ComposeData member '{0}' for '{1}' is ambiguous: more than one inherited or hidden static member matches. Set MemberType to disambiguate, or rename the member. (JIT's texts) |
CMP0052 | not public static; requires arguments; generic member method | ComposeData member '{0}' for '{1}' must be public and static / … requires arguments, which ComposeData does not support / … is a generic method; ComposeData cannot supply type arguments |
CMP0053 | unsupported return type or row element type | ComposeData member '{0}' for '{1}' has unsupported {return/row element} type '{2}'. Expected IEnumerable, Task/ValueTask of IEnumerable, or IAsyncEnumerable of ITheoryDataRow, object?[], or ITuple rows. |
CMP0054 | open-generic MemberType | ComposeData MemberType '{0}' for '{1}' is an open generic type; supply a closed type |
CMP0055 | TConfig statically incompatible with the typed row's element 0 | ComposeData member '{0}' for '{1}': row element 0 type '{2}' can never be the '{3}' profile configuration value |
CMP0056 | statically known too many values; a typed row value that can never be assigned to its parameter | ComposeData member '{0}' for '{1}' supplies {n} row value(s), but the method has {p} parameter(s) / ComposeData member '{0}' for '{1}': row value {i} type '{2}' can never be assigned to parameter '{name}' of type '{3}' |
CMP0057 | invalid active attribute-level skip condition (B4.9) | [ComposeData…] on '{1}' sets both SkipUnless and SkipWhen / [ComposeData…] on '{1}': {SkipUnless/SkipWhen} '{prop}' must be a public static bool property on '{holder}' or one of its base types |
RS1032note: single-sentence diagnostic messages omit the trailing period (all threeCMP0052texts, and likewiseCMP0054,CMP0055, and bothCMP0056/CMP0057texts). Multi-sentence texts (CMP0051,CMP0053) keep it. This is the only intentional deviation from the JIT runtime texts, which append a period.
Reused IDs, through twin DiagnosticDescriptors (same ID, severity, and category as the existing descriptor, with ComposeData-specific wording; the existing [Compose] descriptors, their message formats, and their 18 existing Verify snapshots stay byte-identical, product-owner decision C-3):
CMP0040(twin): the existing signature rules (generic test method, ref/out/in,params, a non-composable parameter shape), plus duplicate[Shared]parameter types (C5; same rule and reason text as JITBindingPlan). No new ID.CMP0042,CMP0046,CMP0047,CMP0048(twins):TProfileconstructor only, forComposeData<TProfile, TConfig>.CMP0044(twin):TProfile,TConfig, the member holder types (lookup type and the member's containing type), and the activeSkipType/skip-condition holder, whenever not accessible from the generated top-level registration (C-2; JIT accepts private nested holders, xUnit's own AOTMemberDatasilently drops them, Compono reports instead).CMP0049(twin):ComposeData<TProfile>'s parameterless constructor.CMP0045(twin): used whenever at least oneComposeDataform is among the stacked attributes; names all six forms (JITBindingPlantext).[Compose]-only stacking keeps the existing descriptor and text.- Never
CMP0041orCMP0043forComposeData:TConfigcomes from the row and is never constructed by generated code (C2).
B5 additions (ADR-0070 Amendment 4 §2, exact text in B5.4): more ComposeData twins:
CMP0054: a member or active skip holder declared on an open generic typeCMP0052: a static abstract or static virtual interface memberCMP0047: a prohibited member, getter, skip property, or skip getter
CMP0053 (existing descriptor) also covers a member type whose IAsyncEnumerable<T> implementations give no deterministic JIT choice (B5.4).
Statically unknown row element types (object?[], ITheoryDataRow, ITuple, object, non-unique enumerable element types) are validated at runtime through the shared helper.
Tasks¶
Execution is sequential per phase. Bead IDs are in the local beads database (epic compono-lsx). Every bead ends with V1 green (0 warnings / 0 errors, all tests passing) unless stated otherwise.
Dependency graph: B0 → B1 → B2 → {B3, B4}. B4 + B1 → B5. B5 + B3 → B6. B3 + B6 → B7 → B8 → B9. After B2, B3 (JIT) and B4–B5 (AOT) can proceed in parallel in separate worktrees if wanted. Sequential in one tree is the default.
Gate¶
B0 (compono-lsx.10): product-owner review¶
- ADR-0070 reviewed and flipped to
Accepted(2026-11-05) - RESEARCH-0038 §10 open items resolved and recorded here in Notes (JIT row-level
DisableParallelizationapproach, theory-level skip edge handling, strict value typing, #166 ordering)
Phase 1: shared helper, public API, JIT¶
B1 (compono-lsx.1, after B0): shared runtime helper, linked into both packages¶
-
src/Shared/Compono.XunitV3/ComposeDataRows.csper the specification, with full XML docs - Link it in both
.csprojs (<Compile Include Link>). ConfirmCOMPONO_XUNITV3_AOTis already defined in the AOT project - Unit tests,
test/Compono.XunitV3.Tests/ComposeDataRowsTests.cs: - collection:
TheoryData<…>,TheoryDataRow<…>[],List<object?[]>,object[]ofobject[],(T1,T2)[]; rejected:string, a singleTheoryDataRow,object[]of scalars, non-enumerable,null - row:
ITheoryDataRowbeforeobject?[]beforeITuple(a type that's bothITheoryDataRowandITupleclassifies asITheoryDataRow); 9-elementValueTupleflattened; nested array value stays one value;nullrow - values:
int?row →int?param (value andnull);null→intparam (error); boxedint→longparam (error, no widening);string?vsstringvs oblivious reference param;DateTimeOffset; subclass instance → base-type param (OK) - config extraction: missing,
null, wrong type, structTConfig - metadata merge: the full grouped-skip matrix (row Skip ± condition × attribute Skip ± condition, including the
SkipTypepassthrough on JIT), row ?? attribute for Label/TestDisplayName/Timeout/Explicit, trait union plusCompono.Seed - messages: exact D1–D5/R0 text; every seed-line variant (none,
Seed: n,not assigned,row.Seed);InnerException/Diagnosticpreservation - Mirror a minimal subset in
test/Compono.XunitV3.Aot.Teststo prove the AOT compilation (public accessibility,EditorBrowsable)
B2 (compono-lsx.2, after B1): public attribute API, both packages¶
-
src/Compono.XunitV3/ComposeDataAttribute.cs,ComposeDataAttribute{TProfile}.cs,ComposeDataAttribute{TProfile,TConfig}.cs: shapes per ADR-0070, withDynamicallyAccessedMembersannotations and XML docs. TheGetDatabody lands in B3. Until then, throwNotSupportedExceptionwith a clear message (never released) -
src/Compono.XunitV3.Aot/ComposeData*.cs: sealed, independent markers, same public shapes - Correction during B4 (ADR-0070 Amendment 3): B2's AOT marker set omitted the
[Shared]parameter marker.src/Compono.XunitV3.Aot/SharedAttribute.cswas added under B4 with the same public contract as the JIT marker, plus the public-API surface test, a focused marker contract test, and the package docs. Actual AOT[Shared]execution remains B5's (see B5) -
PublicApiSurfaceTestsexact-set updates (JIT, and AOT if present, or add an equivalent) - Package
<Description>updates in both.csprojs - Package validation / ApiCompat: an additive-only check against the previous release baseline (
-p:PackageValidationBaselineVersion=<prior>)
B3 (compono-lsx.3, after B2): JIT implementation¶
-
ComposeDataAttribute.GetData: cached per attribute (LazyInitializer) member accessor (reflection: static field, property, or parameterless method onMemberType ?? testMethod.ReflectedType ?? DeclaringType,FlattenHierarchy) and cachedBindingPlan. Per call: invoke the member, awaitTask/ValueTask, enumerate sync orIAsyncEnumerable(withTestContext.Current.CancellationToken, checked between rows). Per row: normalize, validate, build a fresh profile/composer (ConfigProfileBinder.BuildProfilewith a constructor lookup cached once),CreateRow, negative-seed check,[Shared]first, then the rest, then the output row with the seed trait. Wrap failures per R0/D1–D5 -
SupportsDiscoveryEnumeration() => false - Preserve B2's
MemberNameconstructor null guard, and fail clearly for a consumer subclass ofComposeDataAttributethat is not one of the three supported forms (B2 review finding) -
BindingPlanexclusivity: countComposeAttributeandComposeDataAttribute. Update the message to name all six forms - Metadata: build output rows through the shared merge. Pass attribute values from this attribute instance (
ExplicitAsNullable/TimeoutAsNullableare accessible as protected members). Implement JITDisableParallelizationthrough the approved cached-reflection lookup. Test it on xUnit 4.x (honored) and on 3.2.2 (V5: no failure, no-op) -
ComposeMethodDiscovery(generator): addCompono.XunitV3.ComposeDataAttributeplus the`1/`2metadata names, so parameter types get plans and row invokers - Tests (
test/Compono.XunitV3.Tests/ComposeData*Tests.cs; GetData-level plus real-runner): - multiple rows, each distinct seed/trait, values bound, rest composed
- per-row
TConfig(DateTimeOffset) reaches the profile, with no cross-row leakage (profile instance andConfigureper row, asserted with an instance counter) [Shared]with a row-supplied value and with a composed value; shared ordering- nullable params; too many values; type mismatch; null row; unsupported row; config null or wrong type
- sync/
Task/ValueTask/IAsyncEnumerablemembers;MemberType; inherited static member - fixture-dependent values: class and collection fixture through
RequireFixturein a profile factory, and inside the member - parallel isolation: several classes in parallel, each with its own fixture, rows composed independently
- seeds: fresh distinct per row by default; trait equals the seed actually used; attribute
Seedpins all rows; profileWithSeedpins all rows; attributeSeed+ profileWithSeed→ core duplicate-configuration error (no override); negative attribute seed rejected before the member is read; negative profile seed rejected through R0; seed-line variants for pre-CreateRowfailures (Configurethrows with and without attributeSeed, which must never print a random seed), member-level failures (no seed line), post-CreateRowfailures (row.Seed, then re-run withSeed = thatreproduces the same composed values) - metadata, GetData level (asserting the exact
ITheoryDataRowthe runner consumes): attributeTestDisplayName; row/attribute trait union;Timeout;DisableParallelization(row and attribute, on 4.x); attributeSkipUnless/SkipWhen/SkipTypemerge; grouped-skip precedence. Product-owner decision (2026-11-05): for B3 these are sufficient at the GetData level — no runner-level verification is claimed for them. - metadata, real runner: label gives a deterministic display name; row
Skip; attributeSkip; row-level and attribute-level conditionalSkipUnless/SkipWhen(true and false) includingSkipTypereferencing a different condition-holder class; grouped-skip precedence (an unconditional rowSkipoverrides an attribute conditional skip); theory-level[Theory(Skip = …, SkipUnless/SkipWhen = …)]interaction with unconditional rowSkip;Explicit - shapes end to end: one theory per collection/row shape from B1
- mixed
[ComposeData]+[InlineData]on one method: InlineData rows uncomposed and independent - exclusivity:
[Compose]+[ComposeData]→CompositionException - failure in row k: message contains member, index, label, seed;
InnerExceptionpreserved; theory fails as a unit - existing
[Compose]tests unchanged and green (regression) - generator test:
ComposeDatamethods discovered (row-invoker registration emitted)
Phase 2: Native AOT¶
B4 (compono-lsx.4, after B2): AOT discovery and diagnostics¶
B4 is discovery and validation only. It emits no ComposeData execution source. The contract below is the B4 specification amendment (2026-11-06, product-owner decisions C1–C6 and C-1–C-4; evidence: a reflection probe that reproduces JIT BuildMemberAccessor exactly, and decompiled xunit.v3.core.aot 4.0.2 plus its generators, recorded in Notes).
B4.1 Two separate pipelines (raw discovery vs. validated methods).
- Plan generation (raw, unvalidated): three
ComposeMethodDiscoveryproviders forCompono.XunitV3.Aot.ComposeDataAttribute,`1, and`2, using the existing attribute-family-agnosticComposeMethodDiscovery.TransformMethod, folded intocomposeMethodResultsAllexactly like the AOT[Compose]and JITComposeDataproviders. Rule (the existing generator behavior, kept unchanged): every eligible parameter type of every attributed method gets its plan and row-invoker registration whether or not the method passes ComposeData validation, because plans and row invokers are type-keyed, deduplicated across the compilation, and independent of any one method (AOT[Compose]behaves the same way today withCMP0040–CMP0049). Ref/out/in andparamsparameters and non-eligible shapes are skipped, as today. This pipeline's own diagnostics (for exampleCMP0013) are unchanged and may appear alongside a ComposeData diagnostic. A ComposeData diagnostic never removes a plan another method needs. - Validated ComposeData methods: three per-method providers (one per metadata name) that produce the B4.2 model. A method with any diagnostic gets no
ComposeDataexecution registration (B5 must checkDiagnostics.Count == 0, as the AOT[Compose]output step does). In B4 no registration is emitted for any method.
B4.2 Model (AotComposeDataMethodInfo, a sealed record; strings, bools, int?s, and EquatableArray<T> only; no ISymbol, Location, or Compilation):
- form (arity 0/½);
TProfileandTConfigfully qualified names TestDeclaringTypeFqn: the method's containing type (registration key andCreateRowtype, as AOT[Compose]and JITCreateRowuse)LookupTypeFqn:MemberType ?? TestDeclaringTypeMemberContainingTypeFqn: the member's own containing type (the target of the generated call; never the lookup type, see B4.4)MemberName,MethodName- member kind (
Field/Property/Method); return shape (Sync,TaskOf,ValueTaskOf,AsyncEnumerable) - row typing:
Typedwith the element type FQNs in order, orUntyped - parameters:
Name,Fqn,Ordinal,IsNullable(the existingAotComposeMethodDiscovery.IsNullablerule: oblivious reference = non-nullable),IsShared int? Seed: non-null only when theSeednamed argument is present. An explicitSeed = 0is0; a negative value is stored as is (runtime rejection, no compile-time diagnostic, B2 review finding)- attribute metadata, presence-tracked:
Label,Skip,TestDisplayName(string?),Explicit(bool?),Timeout(int?),Traits(nullwhen absent or empty, matching JIT),DisableParallelization(bool?, raw presence; B5 decides rendering) - active skip conditions only (B4.9):
SkipUnlessHolderFqn+SkipUnlessProperty,SkipWhenHolderFqn+SkipWhenProperty Diagnostics(EquatableArray<DiagnosticInfo>)
B4.3 {M}/{T} names. B4 stores only the FQNs above. B5 renders the runtime R0 names on the failure path as typeof(<Lookup>).FullName + "." + MemberName and typeof(<TestDeclaring>).FullName + "." + MethodName. That reproduces JIT's CLR-style text exactly (including + for nested types and the assembly-qualified closed-generic form Roslyn can't produce statically). B6 verifies it under Native AOT. Compile-time CMP messages use Roslyn display names, as every existing AOT diagnostic does.
B4.4 Member resolution. It copies JIT reflection, pinned by the probe table in Notes (rows 01–29).
- Lookup type:
MemberTypewhen present; otherwise the test method's declaring class (C-1). JIT defaults to the concrete reflected test class instead, so an inherited test method whose member is declared only on, or hidden by, a derived class differs between engines. xUnit's AOT runtime keys data factories by the declaring type and gives the factory no derived-class identity, which is exactly how xUnit's own AOTMemberDatabehaves. SettingMemberTypenames one fixed holder class for every derived class. It does not reproduce JIT's per-derived-class selection. It's the workaround only where a fixed holder is appropriate. (ADR-0070 Amendment 2 §1.) - Order: fields, then properties, then parameterless methods, each across the whole base-type chain. Interfaces' base interfaces are not walked.
- Fields: the most-derived public static field named
MemberName. - Properties: public static, non-indexer properties named
MemberName. A more-derived property of the identical type (SymbolEqualityComparer.Default) hides a base one. Two or more remaining → ambiguous. If the selected property's getter isn't public, fall through to methods. - Methods: the most-derived type that declares any public static zero-parameter method named
MemberName(generic or not). Two or more in that type → ambiguous (for exampleCases<T>()+Cases()). A single generic method →CMP0052generic method (C-2). - Instance members and non-public members never take part in selection.
- Failure classification (when nothing was selected):
- no member named
MemberNameon the lookup type itself, and none that is non-private on a base type → not found (CMP0051; a base class's private members are invisible, as in reflection) - otherwise, any method candidate with one or more parameters (including optional-only and
params-only) → requires arguments (CMP0052) - otherwise → not public static (
CMP0052; includesinternal,protected, instance members, and a non-public getter) MemberNamenull or empty →CMP0051not found. An open-genericMemberType→CMP0054.- Generated-call target is
MemberContainingTypeFqn. Probe rows 10/11/21 show thatglobal::<Lookup>.Membercan bind to a different member (a derived method group or instance member).
B4.5 Return shape (from the member's declared type):
IAsyncEnumerable<T>(or a type implementing it) is checked first, so it wins overIEnumerable- non-generic
Task/ValueTask→CMP0053 Task<X>/ValueTask<X>:CMP0053only whenXcan provably never be a non-stringIEnumerable(string, or a sealed or value type that doesn't implementIEnumerable). Otherwise it's valid and checked at runtime (Task<object>is valid)- sync: the declared type must implement
IEnumerableand not bestring. A declaredobject→CMP0053(JIT rejects the declared type when building the accessor)
B4.6 Element types.
- Typed rows are a
TheoryData<…>collection (walking base types, so subclasses count) or a collection whose uniqueIEnumerable<T>/IAsyncEnumerable<T>element isTheoryDataRow<…>. The element FQNs are the generic arguments. - Otherwise the element type is the unique
IEnumerable<T>/IAsyncEnumerable<T>element, or untyped when there isn't one. CMP0053(row element) only when the element type is provably neverITheoryDataRow,object?[], orITuple: a sealed or value type that implements none of them. A reference-type array (string[]) is anobject?[]row (array covariance);int[]is not.
B4.7 Static assignability (C1, typed rows only):
- Unwrap
Nullable<T>on both sides, then require identity, an implicit or explicit reference conversion, boxing, or unboxing (Compilation.ClassifyConversionon the unwrapped types, ignoring nullable annotations). OtherwiseCMP0056(value) orCMP0055(TConfigvs element 0). - Too many values: typed arity (minus 1 for the
TConfigform) greater than the parameter count →CMP0056. Fewer is fine; the remaining parameters are composed. - Null values, and all untyped rows, are checked at runtime (shared helper).
B4.8 Profile checks (C2, twins per "Diagnostics"):
ComposeData<TProfile>:CMP0044(TProfile),CMP0049.ComposeData<TProfile, TConfig>:CMP0042(exactly one usable publicTProfile(TConfig)constructor, the existing rules),CMP0044(TProfileandTConfig), thenCMP0046/CMP0047/CMP0048on theTProfileconstructor only. NeverCMP0041/CMP0043.TConfigmay be any type (DateTimeOffset, an interface, a record with several constructors).
B4.9 Skip conditions (C4, option A; matches the shared helper's grouped merge, ComposeDataRows.CreateDataRow):
- A condition is active only when the attribute's
Skipnamed argument is present and non-null (""counts as set, matching the helper'sis { }). - For each active
SkipUnless/SkipWhen: walkSkipType ?? test declaring classand its base types for a property declaredPublic,static, of typebool(xUnit's own AOTTestMethodDetailsrule). Both set, or not found / not static / not public / notbool→CMP0057. An inaccessible holder →CMP0044. - Inactive conditions (no attribute
Skip) produce no diagnostic and are not recorded, so B5 can never render a reference to a nonexistent property.SkipTypewithout a condition is inert. - Accepted residual (C-4): when the attribute
Skipis set but every row sets its ownSkip, or the member returns no rows, JIT never evaluates the attribute's conditions while AOT still reportsCMP0057.
B4.10 [Shared] (C5): a parameter's marker is the AOT Compono.XunitV3.Aot.SharedAttribute, matched on that fully qualified metadata name (ADR-0070 Amendment 3 - a minimal public marker added under this phase because B2 omitted it; identical public shape to the JIT Compono.XunitV3.SharedAttribute). Two or more IsShared parameters whose types share the same CLR identity - Compilation.ClassifyConversion(a.Type, b.Type).IsIdentity - are a duplicate. That comparison erases nullable-reference annotations (string/string? are identical), treats dynamic as object, and erases tuple element names, while keeping a nullable value type distinct from its underlying type (Box<int?> is not Box<int>) → CMP0040 (twin) with JIT's reason text. B4 discovers and validates the marker only; the generated sharing behavior is B5's.
B4.11 Diagnostic location and precedence.
- Every B4 diagnostic is reported at the test method's location (
LocationInfo.From(method), asCMP0040–CMP0049are). - First failure wins, so at most one ComposeData diagnostic per method, except the existing per-pipeline duplicate of
CMP0045. The fixed order: CMP0045(more than one of the six Compose-family forms; both the[Compose]andComposeDataper-method pipelines count all six forms and both suppress their registration)CMP0040(method signature, then duplicate[Shared])- profile, in the existing helper's order with the
TConfigconstructor gates removed.ComposeData<TProfile>:CMP0044(TProfile), thenCMP0049.ComposeData<TProfile, TConfig>:CMP0044(TConfig),CMP0042,CMP0044(TProfile),CMP0046,CMP0047,CMP0048(the last three on theTProfileconstructor) CMP0054(open-genericMemberType)CMP0051/CMP0052(member resolution, B4.4)CMP0044(member holder types)CMP0053(return shape, then element type)CMP0055, thenCMP0056(count before per-value type)CMP0057, thenCMP0044(skip holder)- Suppression applies only to the ComposeData execution registration. The raw plan-generation pipeline (B4.1) is never suppressed.
B4.12 Out of B4 scope: all emission (B5), {M}/{T} rendering (B5), DisableParallelization rendering (B5), negative-seed message placement (B5), CMP0050 emission isolation (B5), the #166 checkpoint (B5), and every runtime and cross-engine parity check (B6).
B4 acceptance checklist
- Plan pipeline: the three
ComposeMethodDiscoveryAOTComposeDataproviders exist. A composed custom parameter type of an AOT[ComposeData]method gets a generated plan, including when the method fails validation. A plan for a type shared with a valid[Compose]method is still emitted when theComposeDatamethod is invalid - A JIT-namespace
[Compono.XunitV3.ComposeData("Missing")]method (theSeedSampleMethodsshape) produces noCMP0051–CMP0057and no AOTComposeDatamodel - Incremental-caching test: an unrelated edit leaves the
ComposeDataper-method stepsCached/Unchanged(TrackingNames) - Model tests cover every B4.2 field, including
Seedunset →null,Seed = 0→0,Seed = -1→-1with no diagnostic, and every metadata field present vs. absent - Member resolution: one generator test per probe row 01–29 (Notes, 2026-11-06), asserting the outcome recorded there. Valid rows also assert
MemberContainingTypeFqn(rows 10/11/21 → the base class). Inherited test method with a base-declared member: valid. Derived-only member withoutMemberType:CMP0051. WithMemberType: valid - Return/element shapes (B4.5/B4.6). Valid:
Task<object>,ValueTask<TheoryData<…>>,IAsyncEnumerable<object[]>,List<string[]>, aTheoryDatasubclass,IEnumerable<object>.CMP0053:string, a singleTheoryDataRow, non-genericTask,Task<int>, a declaredobject,IEnumerable<int>,IEnumerable<string>.CMP0054:typeof(Holder<>) - Assignability (B4.7). Valid:
int?→int,int→int?,string?→string, subclass→base, elementobject, structTConfigwith elementTConfig?.CMP0056:int→long, too many values with and withoutTConfig.CMP0055: unrelatedTConfig - Profile (B4.8):
CMP0042/CMP0044/CMP0046/CMP0047/CMP0048/CMP0049through the ComposeData twins (wording asserted). No diagnostic forTConfig=DateTimeOffset, an interface, or a two-constructor record -
CMP0040twin: generic test method, ref/out/in,params, duplicate[Shared](string+string?→ diagnostic;int+int?→ none) -
CMP0045: every pairing among the six AOT forms (ComposeData twin text whenever aComposeDataform is involved); no ComposeData model emitted as valid.[InlineData]/[MemberData]alongside[ComposeData]→ no diagnostic - Skip (B4.9): valid active conditions on the test class, a base class, and a
SkipTypeclass → no diagnostic. Inactive conditions (no attributeSkip), including a missing property → no diagnostic and nothing recorded. Active: both set, missing, instance, non-public, non-bool→CMP0057 -
CMP0044twin for a private nestedMemberTypeholder, a holder reached only through a private nested type, and an active privateSkipTypeholder. Internal holders are valid - Precedence (B4.11): one test with several simultaneous faults per adjacent pair in the order, asserting only the first is reported
- All existing
CMP0040–CMP0049Verify snapshots byte-identical (twins, C-3). Existing AOT[Compose]snapshots unchanged -
AnalyzerReleases.Unshipped.md:CMP0051–CMP0057added; theCMP0040/CMP0042/CMP0044–CMP0049notes say they also apply toComposeData -
dotnet build0 warnings / 0 errors;dotnet testgreen
B5 (compono-lsx.5, after B4 and B1): AOT emission¶
Status: Accepted (2026-11-09; product owner, on Claude Opus's independent final acceptance review, Notes). Design approval gate passed. S0 is committed at 5db86fc, S1 at 4b9d12e, S2 at bb67950, S3 at 0680dc2, S4 at 3fcc5d4, S5 at 07ff636f, S6 at 20c1eb9, and S7 at faa09a4; every slice is verified and independently approved, and every B5 acceptance checkbox below is checked. Closing the compono-lsx.5 bead is a separate step. B6–B9 remain pending.
- The B5 design owner specified this on 2026-11-06.
- On the same day the product owner approved ADR-0070 Amendment 4, including the narrowed §2e, and approved this specification for implementation.
B5.1–B5.11 are normative unless a passage is explicitly marked suggested: suggested structure (helper and method decomposition, local names) may change, normative behavior (order, exception filters, texts, the values each call receives) may not. The implementer makes no architectural decision. If the specification doesn't cover a case, or a gate can't be met as written, stop and report to the design owner with the exact evidence (compiler output, test failure, decompiled xUnit source). Don't improvise a workaround.
Evidence base: the B5 architecture-review probes (Notes, 2026-11-06).
B5.1 Ownership and coordination¶
- #166 checkpoint. At specification time (2026-11-06), #166 is OPEN with no ADR or plan. B5 therefore owns the seeded-row scaffold (B5.7). Re-check #166 when S2 starts. If it has gained an
Accepteddesign by then, stop and report; don't adapt to it silently. Record the outcome in Notes. Posting to #166 needs product-owner approval. - AOT
[Shared]execution (ADR-0070 Amendment 3) is B5's: theShareExplicit<T>/ResolveShared<T>ordering in B5.7. - AOT
[Compose]is untouched:AotTheoryDataRowRegistration.scriban,AotTheoryDataRowRegistrationEmitter,AotComposeMethodDiscovery, and their snapshots stay byte-identical. The emission components are built to be liftable but are not wired into[Compose]. - JIT is touched only by the S1 helper extraction (B5.5). No other change to
Compono.XunitV3.
B5.2 Discovery corrections and model additions (slice S0)¶
TConfigconstructor identity. InAotComposeDataMethodDiscovery.FindUsableProfileConstructors, replaceSymbolEqualityComparer.Default.Equals(c.Parameters[0].Type, configType)withcompilation.ClassifyConversion(c.Parameters[0].Type, configType).IsIdentity. Pass in theCompilation. Keep theRefKind.Noneand prohibited-attribute filters unchanged.- Regression tests: valid with no
CMP0042forP((int A, int B))withTConfig = ValueTuple<int, int>,P(dynamic)withTConfig = object, andP(List<dynamic>)withTConfig = List<object>. - Negative control:
P(int?)withTConfig = intstill givesCMP0042with 0 usable constructors. - Don't touch the AOT
[Compose]copy inAotComposeMethodDiscovery(ADR-0070 Amendment 4 §6). - New checks: the B5.4 diagnostics, at the B5.4 precedence.
- Model additions to
AotComposeDataMethodInfo. They hold strings andbools only, take part in record equality, and are populated for every model, failing ones included:
| Field | Value |
|---|---|
TestClassIndex | xUnit's declared-type index for the test method's containing type (B5.3). Equal to FullyQualifiedTestClassName whenever no type in the containment chain is generic |
IsGenericTestClass | true when the containing type or any containing type of it has Arity > 0 |
IsTestClassAccessible | compilation.IsSymbolAccessibleWithin(containingType, compilation.Assembly) |
LookupIsTestClass | true when the MemberType named argument is absent |
AsyncRowElementIsValueType | AsyncEnumerable shape only: true exactly when the declared member type implements one IAsyncEnumerable<T> and T.IsValueType. false for every other shape, including the supported mixed case in B5.4 (one reference element plus value elements) |
- Incremental caching: the existing B4
TrackingNamescaching test must still show the per-method steps asCached/Unchangedafter an unrelated edit.
B5.3 Generic test-class bases (ADR-0070 Amendment 4 §1)¶
- Index renderer (one internal static method, unit-tested). Start with
global::plus the containing namespace rendered withSymbolDisplayFormat.FullyQualifiedFormatminus itsglobal::prefix. A type in the global namespace gets no namespace segment. Then render each type in the containment chain, outermost first, joined with.: - each type's name is escaped exactly as
FullyQualifiedFormatescapes keywords (@prefix); - a type with
Arity > 0is followed by<+(Arity − 1)commas +>.
Don't use FullyQualifiedFormat on the type itself (it renders <T>), and don't use ConstructUnboundGenericType() (undefined for a non-generic type nested in a generic one).
| Declaring type | Required index (verified against xUnit 4.0.2) |
|---|---|
Ns.GenericBase<T> | global::Ns.GenericBase<> |
Ns.Two<T1, T2> | global::Ns.Two<,> |
Ns.OuterPlain.InnerGen<T> | global::Ns.OuterPlain.InnerGen<> |
Ns.Outer<T>.InnerBase | global::Ns.Outer<>.InnerBase |
| non-generic chain (incl. a keyword-named type and a global-namespace type) | byte-identical to FullyQualifiedFormat (the existing AOT [Compose] key) |
- Registration:
RegisterTheoryDataRowFactory(<TestClassIndex literal>, <MethodName literal>, disableDiscoveryEnumeration: true, factory). testClasslocal, computed once per factory execution (never at registration, never per row):- non-generic:
typeof(<FullyQualifiedTestClassName>) - generic:
global::Compono.XunitV3.Aot.ComposeDataRows.ResolveGenericTestClassType(typeof(<TestClassIndex>))
The index text is valid inside typeof for all four shapes. testClass is the only value used for CreateRow, every descriptor's DeclaringType, {T}, and {M} when LookupIsTestClass. - No open-generic fallback anywhere (Amendment 4 §1). - Always register for a generic declaring type. Add no special handling for the xUnit1063/xUnit1032 shapes. - Inaccessible test class (IsTestClassAccessible == false, a private or protected nested class, xUnit1000 on both engines): emit nothing. Report the model's diagnostics as usual. No new diagnostic.
B5.4 Diagnostics (additive twins; exact)¶
New DiagnosticDescriptor objects only. Existing descriptors, their messages, and their Verify snapshots stay byte-identical (C-3). Every entry is Error, category Compono.Usage. Each is single-sentence, so there's no trailing period (RS1032). AnalyzerReleases.Unshipped.md is keyed by ID, so it gets no new rows. Update the existing CMP0047/CMP0052/ CMP0053/CMP0054 notes to mention the new ComposeData conditions.
Argument conventions match B4:
{memberDisplay}is B4'smemberDisplayName: theMemberType(or declaring type) display plus.plus the member name.{method}is B4'smethodDisplayName.- Type and holder texts are
ToDisplayString(). {attr}is the existingProhibitedCallSiteAttributerendering:[Obsolete(error: true)],[Experimental("ID")], or[CompilerFeatureRequired("Feature")]. Generalize the helper fromIMethodSymboltoISymbol. Its rules are unchanged, soCompilerFeatureRequiredwithIsOptional = trueis still ignored.
| ID | Descriptor | Title | Message format (args) |
|---|---|---|---|
CMP0054 | ComposeDataOpenGenericMemberHolder | ComposeData member is declared on an open generic type | ComposeData member '{0}' for '{1}' is declared on open generic type '{2}', which generated code cannot name; set MemberType to a non-generic or closed generic type that declares the member ({0} memberDisplay, {1} method, {2} the member's containing type) |
CMP0054 | ComposeDataOpenGenericSkipHolder | ComposeData skip condition is declared on an open generic type | [ComposeData] on '{0}': {1} '{2}' is declared on open generic type '{3}', which generated code cannot name; set SkipType to a non-generic or closed generic type that declares the property ({0} method, {1} SkipUnless/SkipWhen, {2} property name, {3} the property's containing type) |
CMP0052 | ComposeDataStaticInterfaceMember | ComposeData member is a static abstract or static virtual interface member | ComposeData member '{0}' for '{1}' is a static abstract or static virtual interface member, which generated code cannot call; move the data to a static member of a class or struct |
CMP0052 | ComposeDataStaticInterfaceSkipProperty (added in S5, ADR-0070 Amendment 4 §7) | ComposeData skip condition property is a static abstract or static virtual interface member | [ComposeData] on '{0}': {1} '{2}' on '{3}' is a static abstract or static virtual interface property, which generated code cannot read; move the condition to a static property of a class or struct, or to a non-virtual static interface property ({0} method, {1} SkipUnless/SkipWhen, {2} property name, {3} the property's containing type) |
CMP0047 | ComposeDataProhibitedMember | ComposeData member cannot be used at its generated call site | ComposeData member '{0}' for '{1}' is marked {2}, which makes any use of it a compiler error; the generated registration reads the member directly, so it would fail to compile |
CMP0047 | ComposeDataProhibitedMemberGetter | ComposeData member getter cannot be used at its generated call site | ComposeData member '{0}' for '{1}' has a getter marked {2}, which makes any use of it a compiler error; the generated registration reads the member directly, so it would fail to compile |
CMP0047 | ComposeDataProhibitedSkipProperty | ComposeData skip condition property cannot be used at its generated call site | [ComposeData] on '{0}': {1} '{2}' on '{3}' is marked {4}, which makes any use of it a compiler error; the generated registration reads the property directly, so it would fail to compile |
CMP0047 | ComposeDataProhibitedSkipPropertyGetter | ComposeData skip condition getter cannot be used at its generated call site | [ComposeData] on '{0}': {1} '{2}' on '{3}' has a getter marked {4}, which makes any use of it a compiler error; the generated registration reads the property directly, so it would fail to compile |
CMP0053 | existing ComposeDataUnsupportedReturnOrRowElementType (reused unchanged, return variant) | — | for a declared member type that implements IAsyncEnumerable<T> for two or more distinct T (Amendment 4 §2e) |
Predicates:
- Open generic: a type is open when it is an
INamedTypeSymbolthat isIsUnboundGenericType, or any type argument contains a type parameter (ComposedTypeAnalyzer.ContainsTypeParametersemantics), or itsContainingTypeis open, checked recursively. The existing Step 4MemberTypeCMP0054check is unchanged. - Static interface member: the selected member's
ContainingTypehasTypeKind.Interface, and the member (the property symbol for properties)IsStatic && (IsAbstract || IsVirtual). - Static interface skip property (S5, Amendment 4 §7): the same predicate (reuse
IsStaticInterfaceMember), applied to the resolved active skip property returned byResolveSkipProperty. A plain (non-virtual, non-abstract) static interface property doesn't match and stays supported through directglobal::IFoo.Paccess. A property found only on a base interface ofSkipType, or on an interface implemented by a classSkipType, isn't resolved at all (ResolveSkipPropertywalksBaseTypeonly) and keeps the existingCMP0057. Don't change that lookup. - Async interface set (ADR-0070 Amendment 4 §2e). Collect the distinct constructed
IAsyncEnumerable<T>from the declared member type'sAllInterfaces, plus the type itself when it isIAsyncEnumerable<>. Let R be the number with a reference-typeTand V the number with a value-typeT. Like JIT'sTryGetAsyncEnumerableElementType, any non-empty set makes the shapeAsyncEnumerable, checked first inAnalyzeReturnShape, so it wins overTask/IEnumerable. Then:
| R | V | Result |
|---|---|---|
| 1 | 0 | supported; element = that T (unchanged B4 behavior) |
| 0 | 1 | supported; element = that T; AsyncRowElementIsValueType = true (unchanged) |
| 1 | ≥ 1 | supported (new): JIT always reads the single reference implementation through its IAsyncEnumerable<object?> fast path. Element for B4.6/B4.7 row analysis = the reference T; AsyncRowElementIsValueType = false |
| ≥ 2 | any | CMP0053 (return): more than one implementation matches the IAsyncEnumerable<object?> variance cast |
| 0 | ≥ 2 | CMP0053 (return): JIT's choice depends on Type.GetInterfaces() order |
Precedence (extends B4.11). Existing steps and their internal order are unchanged; the new checks are appended inside the step they belong to:
- Step 6 (holder accessibility, unchanged), then:
- 6a
CMP0054open-generic member holder - 6b
CMP0052static interface member - 6c
CMP0047member, thenCMP0047getter (properties only) - Step 7
CMP0053, now starting with the multiple-async-interfaces check. - Step 9, per active condition in the existing order (
SkipUnlessthenSkipWhen):CMP0057→CMP0044(existing), then 9aCMP0054open-generic skip holder (the resolved property's containing type), then 9bCMP0052static interface skip property (added in S5, mirroring 6a > 6b > 6c), then 9cCMP0047skip property, thenCMP0047skip getter. (S0 shipped 9c as "9b". The relative order of the S0 checks doesn't change.)
There is still at most one ComposeData diagnostic per method.
S0 tests (generator, AotComposeData*Tests). Each test asserts the exact message.
- Each new condition:
- member on
GenericBase<T>reached through the default lookup - member on
GenericBase<T>withMemberType = typeof(Holder)(valid) - member on a non-generic
Rootbase ofGenericBase<T>(valid) - active skip property on
GenericBase<T> - active skip property with
SkipType = typeof(Holder<>) - static abstract and static virtual interface members through
MemberType = typeof(IFoo) - each prohibited attribute on a field, a property, a property getter, a method, an active skip property, and a skip getter
- plain
[Obsolete](valid, no diagnostic) - the async interface table, one test per row:
stringonly;intonly;int+string(valid, model elementstring,AsyncRowElementIsValueType == false; a typedTheoryDataRow<…>reference element still gets the B4.7 checks);string+Uri→CMP0053;int+long→CMP0053; also a type that implementsIAsyncEnumerable<int>andIEnumerable(stillAsyncEnumerable, as on JIT) - One adjacent-precedence test per new boundary:
- 6 > 6a > 6b > 6c > 7
- 8 > 9's
CMP0057>CMP0044> 9a > 9c (S0); S5 adds 9a > 9b > 9c - Index renderer: a unit test for every row of the B5.3 table.
- Model: a test for every new field.
- Regression: every existing B4 test and Verify snapshot passes unchanged. If an existing expected diagnostic changes, stop and report.
B5.5 Helper contracts (src/Shared/Compono.XunitV3/ComposeDataRows.cs, slice S1)¶
Same accessibility pattern as the existing members: internal on JIT, public + [EditorBrowsable(EditorBrowsableState.Never)] on AOT. Move each text verbatim from ComposeDataAttribute.cs, including its interpolation and culture behavior.
static CompositionException MemberReadFailure(string member, string methodDisplayName, Exception exception)
{
var cause = exception is System.Reflection.TargetInvocationException { InnerException: { } inner } ? inner : exception;
return new CompositionException(
$"ComposeData member '{member}' for '{methodDisplayName}' failed while reading its value: {cause.Message}",
(cause as CompositionException)?.Diagnostic,
cause); // ADR-0070 Amendment 1 constructor; NO seed line (member level)
}
static CompositionException NegativeAttributeSeed(int seed) =>
new($"Compono.XunitV3 requires a non-negative seed, but the configured seed was {seed}." + PreRowSeedLine(seed));
static CompositionException NegativeRowSeed(in RowContext context, int rowSeed) =>
RowFailure(in context, $"Compono.XunitV3 requires a non-negative seed, but the configured seed was {rowSeed}.", RowSeedLine(rowSeed));
#if COMPONO_XUNITV3_AOT
static Type ResolveGenericTestClassType(Type genericTypeDefinition)
{
for (var type = (TestContext.Current.TestClass as Xunit.v3.ICoreTestClass)?.Class; type is not null; type = type.BaseType)
{
if (type.IsGenericType && type.GetGenericTypeDefinition() == genericTypeDefinition)
return type;
}
throw new InvalidOperationException(
$"ComposeData could not resolve the closed generic test class for '{genericTypeDefinition}' from the executing test class.");
}
#endif
- Message text. The literal "Compono.XunitV3" stays in the negative-seed text on AOT too, for byte parity.
- JIT switch-over (the only JIT edit in B5):
GetDataCore's attribute check callsthrow ComposeDataRows.NegativeAttributeSeed(negativeSeed);ComposeRow's check callsthrow ComposeDataRows.NegativeRowSeed(context, row.Seed);- the private
MemberReadFailureis deleted, and its callers use the shared one.
The JIT suite must pass unchanged: its exact-text assertions are the byte-parity proof. - Public surface. Update the AOT PublicApiSurfaceTests for exactly these four members. Package validation / ApiCompat stays additive-only. - Other failures reuse the existing helpers: NullResult, UnsupportedReturnType, IsSupportedCollectionResult, NormalizeRow, ExtractConfig<TConfig>, BindValue<T>, TooManyValues, RowFailure, PreRowSeedLine, RowSeedLine, and CreateDataRow. Add no other helper. - S1 tests (ComposeDataRowsTests, both projects as in B1): - exact text of all three messages - TargetInvocationException unwrap - InnerException and the Diagnostic reference preserved - no seed line on MemberReadFailure - NegativeAttributeSeed(-1) ends with "\n\nSeed: -1" - NegativeRowSeed with and without a label - AOT only: ResolveGenericTestClassType - from a [Fact] in a closed subclass of a generic base it returns the closed base - with two closed subclasses (<int>, <string>) each returns its own closed base - from a non-generic test class it throws the exact InvalidOperationException text
B5.6 Generated registration: template, emitter, wiring¶
- New files:
src/Compono.Generators/Templates/AotComposeDataRegistration.scribansrc/Compono.Generators/Emitters/AotComposeDataRegistrationEmitter.cs- hint name
GeneratedFileNaming.HintNameFor($"{FullyQualifiedTestClassName}.{MethodName}") + ".AotComposeDataRegistration.g.cs" - Wiring: the existing
aotComposeDataMethodsAllRegisterSourceOutputreports diagnostics as today, then: if (method.Diagnostics.Count > 0 || !method.IsTestClassAccessible) return;EmissionIsolation.TryEmit(productionContext, "AOT ComposeData registration", $"{method.FullyQualifiedTestClassName}.{method.MethodName}", () => AotComposeDataRegistrationEmitter.Generate(productionContext, method))
An emission failure therefore surfaces as CMP0050 for that method only (ADR-0068). - Emission gate. Every path that can emit a registration gates on the single predicate AotComposeDataRegistrationEmitter.IsWithinEmissionEnvelope - the single authoritative emission-eligibility check. It was introduced in slice S2 (bb67950) as IsWithinS2Envelope and renamed in slice S3 (0680dc2) when S3 widened it. As first introduced (S2) it admitted only the non-generic test class; plain [ComposeData]; the Sync member shape; untyped and typed rows; no profile, no Seed, no [Shared], no active skip predicate; constants-only metadata - the S2 checkpoint Notes below record that historical form and name. S3 widened the envelope to admit the profile (<TProfile>), profile-config (<TProfile, TConfig>), attribute Seed (including a negative seed, whose factory body throws at execution time), and [Shared] forms, while the envelope still excludes the S4 (TaskOf/ValueTaskOf/AsyncEnumerable), S5 (active SkipUnless/ SkipWhen), and S6 (generic test-class bases) shapes: those still produce no registration (and no invalid generated source) rather than a registration that silently drops the feature's semantics. S4–S6 must each explicitly widen IsWithinEmissionEnvelope as they implement their own functionality - a later slice must not emit a shape whose semantics it does not yet reproduce. - No Scriban partials or includes. TemplateHelper has no loader, and adding one is out of scope. "Reusable components" are named, commented template sections plus small view-model builder methods in the emitter: member read, row reader per shape, seeded-row scaffold, parameter binding, and metadata. - File rules: - every type global::-qualified - member and skip-property names in C# positions go through RequiredMemberCollector.EscapeIdentifier; in string literals they stay raw, through SymbolDisplay.FormatLiteral - no LINQ - arrays and lists pre-sized where the count is known - lambdas static unless they capture config - ConfigureAwait(false) on every await - Call targets: - the member is read through MemberContainingTypeFqn, never the lookup type - a skip condition is read through its recorded holder - a typed row element type is never named (B4 hand-off); binding uses parameter types only
Shape. Normative: the file-level elements (header, pragmas, file class, [ModuleInitializer] registration call and its arguments), the order of operations, the exception filters, and the values each call receives. Suggested: local names and the decomposition into private methods. The example is ComposeData<TProfile, TConfig>, a sync member, a non-generic test class, Seed = 42, and a [Shared] parameter at ordinal 2.
// <auto-generated/>
#nullable enable
// CS0612/CS0618: a warning-level [Obsolete] data member is valid on JIT (Amendment 4 §2c);
// CS1998: sync member shapes produce an async method with no await.
#pragma warning disable CS0612, CS0618, CS1998
[global::System.CodeDom.Compiler.GeneratedCode("Compono.Generators", "<version>")]
file static class AotComposeDataRegistration
{
[global::System.Runtime.CompilerServices.ModuleInitializer]
internal static void Register() =>
global::Xunit.v3.RegisteredEngineConfig.RegisterTheoryDataRowFactory(
"global::Ns.PricingTests",
"Prices_by_date",
disableDiscoveryEnumeration: true,
factory: static _ => CreateRowsAsync());
private static async global::System.Threading.Tasks.ValueTask<global::System.Collections.Generic.IReadOnlyCollection<global::Xunit.ITheoryDataRow>> CreateRowsAsync()
{
var cancellationToken = global::Xunit.TestContext.Current.CancellationToken;
var testClass = typeof(global::Ns.PricingTests); // generic: ResolveGenericTestClassType (B5.3)
var memberDisplayName = testClass.FullName + ".PricingCases"; // !LookupIsTestClass: typeof(<Lookup>).FullName
var methodDisplayName = testClass.FullName + ".Prices_by_date";
var attributeMetadata = new global::Compono.XunitV3.Aot.ComposeDataAttributeMetadata { /* B5.8 */ };
var rawRows = ReadRows(memberDisplayName, methodDisplayName, cancellationToken); // async shapes: await ReadRowsAsync(...)
var result = new global::Xunit.ITheoryDataRow[rawRows.Count];
for (var index = 0; index < result.Length; index++)
{
result[index] = ComposeRow(rawRows[index], index, testClass, memberDisplayName, methodDisplayName, in attributeMetadata);
}
return result;
}
// B5.6 member read: one generic helper for every shape; wraps everything except cancellation.
private static T ReadMember<T>(global::System.Func<T> read, string memberDisplayName, string methodDisplayName)
{
try
{
return read();
}
catch (global::System.Exception exception) when (exception is not global::System.OperationCanceledException)
{
throw global::Compono.XunitV3.Aot.ComposeDataRows.MemberReadFailure(memberDisplayName, methodDisplayName, exception);
}
}
// ReadRows / ReadRowsAsync per shape: B5.6 "Row readers". ComposeRow: B5.7.
}
When the attribute Seed is negative (a compile-time constant), CreateRowsAsync's whole body is throw global::Compono.XunitV3.Aot.ComposeDataRows.NegativeAttributeSeed(<seed>);. No row reader and no ComposeRow is emitted, so there's no unreachable code and no CS0162. The async method turns the throw into a faulted ValueTask, the same as JIT's async GetDataCore.
Row readers. Emit only the one for the model's shape. The behavior of each reader (order, filters, null and shape checks) is normative and mirrors B5.9. Its decomposition into ReadMember/AppendSync/ ReadValueRowsAsync is suggested. <read> is static () => global::<MemberContainingType>.<EscapedName> for a field or property, plus () for a method.
Sync:object? source = ReadMember(<read>, …);- then a shared
AppendSync(object? source, …)that:- throws
NullResultwhensourceis null - throws
UnsupportedReturnType(…, source.GetType())when!IsSupportedCollectionResult(source) - enumerates
(global::System.Collections.IEnumerable)source, withcancellationToken.ThrowIfCancellationRequested()before each add - wraps the loop in
catch (Exception e) when (e is not CompositionException and not OperationCanceledException)→MemberReadFailure
- throws
TaskOf:var pending = ReadMember(<read>, …);if (pending is null) throw NullResult(…);object? awaited; try { awaited = await pending.ConfigureAwait(false); } catch (Exception e) when (e is not CompositionException and not OperationCanceledException) { throw MemberReadFailure(…); }- then
AppendSync(awaited, …) ValueTaskOf: the same asTaskOf, with no null check (aValueTask<>is never null; JIT's boxed check never fires).AsyncEnumerable,AsyncRowElementIsValueType == false(JIT's fast path):object? source = ReadMember(<read>, …);if (source is null) throw NullResult(…);try { await foreach (var row in ((global::System.Collections.Generic.IAsyncEnumerable<object?>)source).WithCancellation(cancellationToken).ConfigureAwait(false)) { cancellationToken.ThrowIfCancellationRequested(); rows.Add(row); } }catch (Exception e) when (e is not CompositionException and not OperationCanceledException)→MemberReadFailureAsyncEnumerable,AsyncRowElementIsValueType == true(JIT's reflection path, reproduced statically):var source = ReadMember(<read>, …);if ((object?)source is null) throw NullResult(…);- Runtime check first, exactly as JIT does: if
(object)source is global::System.Collections.Generic.IAsyncEnumerable<object?>, take the reference reader above. A runtime subclass can add a reference implementation to a value-element declared type, and JIT decides by the runtime object, not the declared type. - otherwise call the file-local
ReadValueRowsAsync<TElement>(global::System.Collections.Generic.IAsyncEnumerable<TElement> source, …).TElementis inferred, never named; the B5.4 async-interface table guarantees the declared type has exactly one implementation.
Its body:
global::System.Collections.Generic.IAsyncEnumerator<TElement> enumerator;
try { enumerator = source.GetAsyncEnumerator(cancellationToken); }
catch (global::System.Exception e) when (e is not global::System.OperationCanceledException)
{ throw global::Compono.XunitV3.Aot.ComposeDataRows.MemberReadFailure(memberDisplayName, methodDisplayName, e); }
var rows = new global::System.Collections.Generic.List<object?>();
global::System.Exception? failure = null;
try
{
while (await enumerator.MoveNextAsync().ConfigureAwait(false))
{
cancellationToken.ThrowIfCancellationRequested();
rows.Add(enumerator.Current);
}
}
catch (global::System.Exception e) { failure = e; }
try { await enumerator.DisposeAsync().ConfigureAwait(false); }
catch (global::System.Exception disposeException) { failure ??= disposeException; }
if (failure is not null)
{
if (failure is global::System.OperationCanceledException or global::Compono.CompositionException)
global::System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(failure).Throw();
throw global::Compono.XunitV3.Aot.ComposeDataRows.MemberReadFailure(memberDisplayName, methodDisplayName, failure);
}
return rows;
B5.7 Per-row scaffold (ComposeRow): seeded rows, binding, [Shared]¶
Order and filters are exactly JIT's ComposeRow. attributeSeed below is the literal int or null, P is the parameter count, and RowContext and ComposeDataRows are global::Compono.XunitV3.Aot.*.
var initialContext = new RowContext(index, null, memberDisplayName, methodDisplayName);var rowData = ComposeDataRows.NormalizeRow(rawRow, in initialContext, attributeSeed);var context = new RowContext(index, rowData.Source, memberDisplayName, methodDisplayName);TConfigform:var (config, values) = ComposeDataRows.ExtractConfig<global::TConfig>(rowData.Values, in context, attributeSeed);Other forms:var values = rowData.Values;- Build the profile, composer, and row in one block:
global::Compono.CompositionRow row;
try
{
var composer = global::Compono.Composer.Create(builder =>
{
builder.AddProfile(new global::P(config)); // <TProfile>: builder.AddProfile<global::P>(); no profile: omit
builder.WithSeed(42); // only when Seed is set (non-negative); always AFTER AddProfile (JIT order)
});
row = composer.CreateRow(testClass);
}
catch (global::System.Exception exception) when (exception is not global::System.OperationCanceledException)
{
throw ComposeDataRows.RowFailure(in context, exception.Message, ComposeDataRows.PreRowSeedLine(attributeSeed), exception);
}
- Plain
[ComposeData]with noSeedusesglobal::Compono.Composer.Create()inside the sametry. - The lambda is
staticwhen it captures nothing. new P(config)passes theTConfig-typed local with no cast. A cast to the constructor's declared parameter type could be(dynamic), which would bind dynamically and isn't AOT-safe. Identity conversion already selects the validated constructor, and B4'sCMP0048covers priority siblings.if (row.Seed < 0) throw ComposeDataRows.NegativeRowSeed(in context, row.Seed);if (values.Length > P) throw ComposeDataRows.TooManyValues(in context, values.Length, P, row.Seed);- Bind every supplied value before composing anything, in ordinal order. For each parameter
i: - declare
global::Ti value_i = default!; if (values.Length > i) value_i = ComposeDataRows.BindValue<global::Ti>(values, i, <IsNullable>, "<name>", in context, row.Seed);- Compose, in one
try { … } catch (global::Compono.CompositionException exception) { throw ComposeDataRows.RowFailure(in context, exception.Message, ComposeDataRows.RowSeedLine(row.Seed), exception); }. OnlyCompositionExceptionis wrapped, as on JIT. - First, every
IsSharedparameter in ordinal order:if (values.Length > i) row.ShareExplicit<global::Ti>(<descriptor i>, value_i);else value_i = row.ResolveShared<global::Ti>(<descriptor i>);
- Then every non-shared parameter in ordinal order:
if (values.Length <= i) value_i = row.Resolve<global::Ti>(<descriptor i>); <descriptor i>isnew global::Compono.CompositionRequestDescriptor(global::Compono.CompositionRequestKind.TestParameter, i, "<name>", testClass, global::Compono.Nullability.<Nullable|NotNullable>).return ComposeDataRows.CreateDataRow(new object?[] { value_0, …, value_P-1 }, in context, in attributeMetadata, row.Seed);
Each row gets a fresh profile, composer, and row (ADR-0070 item 6). Nothing is cached across rows or executions.
B5.8 Attribute metadata rendering¶
attributeMetadata is built once per factory execution, as on JIT, with every member assigned explicitly so snapshots are deterministic:
| Member | Rendered from the B4 model |
|---|---|
Explicit | true/false literal, or null |
Label, Skip, TestDisplayName | FormatLiteral string, or null |
Timeout | int literal, or null |
Traits | null when absent or empty, else new string[] { "name", "value", … } (B4's flat pair order) |
DisableParallelization | the B4 bool? value ?? false: false when absent. This matches JIT on xUnit 4.x, which always supplies the attribute's non-null bool, so the merged output row is identical (Amendment 4 §5) |
SkipUnless, SkipWhen | null, or static () => global::<HolderFqn>.<EscapedProperty> for a recorded active condition only (B4.9) |
The single merge rule stays in ComposeDataRows.CreateDataRow. Never call DataAttributeRegistration.CreateDataRow.
B5.9 JIT stage table (exception filters and order - must match exactly)¶
| Stage | Behavior |
|---|---|
| Factory start | negative attribute Seed → NegativeAttributeSeed (whole body, B5.6); then {M}/{T} and the metadata once |
| Calling the member (every shape) | ReadMember: wraps everything except OperationCanceledException, including CompositionException |
Task<X> result is null | NullResult (D5); never await a null task |
Awaiting Task<X>/ValueTask<X> | wraps everything except CompositionException and OperationCanceledException |
| Sync or awaited collection | null → D5; !IsSupportedCollectionResult → D4 with the runtime type; enumerate through non-generic IEnumerable, ThrowIfCancellationRequested() before each add; enumeration errors wrapped except CompositionException/OperationCanceledException |
| Async path selection | by the runtime object, as JIT: source is IAsyncEnumerable<object?> → reference row; otherwise the value row (reachable only when the declared type's single implementation has a value-type element) |
| Async, reference element | await foreach over IAsyncEnumerable<object?> with WithCancellation; a cancellation check per row; the same filter. A CompositionException from GetAsyncEnumerator passes through; a DisposeAsync failure replaces the original |
| Async, value element | manual loop (B5.6): GetAsyncEnumerator wraps everything except OperationCanceledException; the original failure wins over a dispose failure; OperationCanceledException/CompositionException rethrown as-is |
| Reading rows | all raw rows are read before any row is composed; no cancellation check while composing |
| Per row | B5.7 steps 1–8 in order |
This deliberately reproduces JIT's two async behaviors, which differ from each other. Normalizing them would change B3's behavior and is out of scope (see Notes: the 2026-11-06 B5 specification entry, decision ©, and the S4 checkpoint entry's known limitations).
B5.10 Test harness for real-runner gates (test/Compono.XunitV3.Aot.Tests)¶
Support/RegisteredComposeDataFactory.cs:- an
internal statichelper withInvokeAsync(string testClassIndex, string methodName) - it gets factories through the public
global::Xunit.v3.RegisteredEngineConfig.GetTheoryDataRowFactories(ICodeGenTestMethod) - its private stub class implements
ICodeGenTestMethod:DeclaredTypeIndexandMethodNamereturn the arguments, and every other member throwsNotSupportedException - it asserts exactly one registered factory with
DisableDiscoveryEnumeration == true - it invokes the factory with
new DisposalTracker()and returns the rows
It must be called from inside a running test, which supplies TestContext.Current. The project doesn't reference NSubstitute, so don't add it. If the interface can't be stubbed, stop and report. - Two kinds of target method: - Success-path targets are ordinary [Theory] + [ComposeData…] methods that the real runner executes, with assertions in the theory body. - Failure-path and metadata targets are plain [ComposeData…] methods without [Theory]. Their factory is still registered by the module initializer, but the runner does not discover the method as a theory, so it is never executed as one. A [Fact] invokes the actual registered generated factory through the harness and asserts exact messages, InnerException/Diagnostic, row counts, labels, traits (Compono.Seed), and merged metadata.
**Approved S2 deviation (2026-11-07).** The original plan used
`[Theory(Explicit = true)]` so the runner would not execute the targets
by default. That is wrong for the xUnit AOT runner: the xUnit AOT
runner invokes the row factory even for `[Theory(Explicit = true)]`
methods during a default test run. For intentionally failing factories,
this causes the theory to fail before the harness can independently
validate the expected exception. Claude Opus reproduced this behavior
on net9.0, net10.0, and net11.0. Plain `[ComposeData]` (no `[Theory]`)
is the approved pattern. **B6 must reuse this pattern for its
failure-path and metadata assertions. Risk:** a future diagnostic that
requires `[Theory]` alongside `[ComposeData]` could invalidate it; if
that arises, stop and report to the design owner.
- Generic targets: the invoking
[Fact]lives in the closed subclass (e.g.IntTests : GenericBase<int>), soTestContext.Current.TestClassis the derived class. Also assert that the factory is found under the exact"global::Ns.GenericBase<>"index. - File: B5's tests go in
test/Compono.XunitV3.Aot.Tests/ComposeDataEmissionTests.cs. B6 later adds the full behavioral matrix inComposeDataTests.csand reuses the harness. (Reduced to the B6 Pass 1 gap-fill set by D2, 2026-11-10.) - Generator snapshot tests (
Compono.Generators.Tests) assert the generated text withGeneratorTestHelpers.VerifyGeneratedSource- the B5.10 compile-free snapshot helper (S2). It snapshots the driver's generated source without recompiling it, and still fails when the generator reported any diagnostic, so aCMP0050emission failure fails the test rather than silently snapshotting nothing. They do not compile the generated code against a stand-inComposeDataRows; a stand-in would drift from the real helper. Real generated-code compilation is validated separately by the AOT test project build.
B5.11 Implementation slices and gates¶
Each slice is independently reviewable and must pass every gate before the next starts. Record the gate evidence in Notes. Don't commit without product-owner approval.
Common gates (G):
- G1: full-solution
dotnet buildwith 0 warnings and 0 errors. - G2: full-solution
dotnet testgreen. Every pre-existing Verify snapshot byte-identical (git statusshows no modified*.verified.*), and the AOT[Compose]files untouched. - G3: new Verify snapshots for the slice's shapes, with no
CMP0050. - G4:
test/Compono.XunitV3.Aot.Testsbuilds the slice's generated code against the realCompono,Compono.XunitV3.Aot, and xUnit AOT assemblies on every TFM it targets. - G5: the slice's tests run under the real xUnit AOT runner (
dotnet test test/Compono.XunitV3.Aot.Tests) and pass, including the harness assertions.
| Slice | Scope | Gates |
|---|---|---|
| S0 | B5.2 + B5.4 (discovery only; no emission) | G1, G2, plus the B5.4 S0 tests |
| S1 | B5.5 helpers and the JIT switch-over; AOT PublicApiSurfaceTests | G1, G2; the JIT suite unchanged and green; AOT S1 tests |
| S2 | B5.6 wiring and template; non-generic test class; plain [ComposeData]; Sync shape (field, property, method; inherited member; MemberType); untyped and typed (TheoryData<…>) rows; B5.7 without a profile; B5.8 with constants only; Compono.Seed trait; R0, too-many, null/type-mismatch values (the profile-driven negative row seed is tested in S3) | G1–G5 |
| S3 | <TProfile>, <TProfile, TConfig> (incl. the S0 identity shapes executed for real), attribute Seed (positive, 0, negative-throw body), duplicate-seed conflict, every seed-line variant, profile WithSeed negative → NegativeRowSeed, [Shared] (row value and composed; ordering witness) | G1–G5 |
| S4 | TaskOf (incl. null task → D5), ValueTaskOf, AsyncEnumerable reference and value element paths, the full B5.9 filter matrix (a member that throws CompositionException when invoked → wrapped; while awaiting → passes through; at GetAsyncEnumerator reference → passes through, value → wrapped; DisposeAsync failure precedence on each path), cancellation mid-enumeration → OperationCanceledException; async path selection: a declared int + string type reads the string rows; a declared IAsyncEnumerable<int> whose runtime object also implements IAsyncEnumerable<string> reads the string rows (JIT parity) | G1–G5 |
| S5 | B5.8 full metadata: active SkipUnless/SkipWhen lambdas (test class, base class, SkipType class), grouped-skip precedence, Explicit, Timeout, traits union, DisableParallelization (absent → false, true, row override), keyword-named member and skip property (@default), a plain [Obsolete] member under TreatWarningsAsErrors. Added by the S5 review (2026-11-09, Notes; ADR-0070 Amendment 4 §7): (a) the B5.4 CMP0052 static-interface skip-property twin at precedence 9b. Generator tests assert the exact message for each of: a static virtual interface property (for both SkipUnless and SkipWhen); a static abstract interface property; a static virtual property on an interface that also carries an unimplemented static abstract member; and a static virtual property on a closed generic interface. Adjacent precedence: an open generic interface holder → CMP0054 (9a > 9b), and a static virtual property marked [Obsolete(error: true)] → CMP0052 (9b > 9c). A plain (non-virtual) static interface property is supported: a Verify snapshot plus a real-runner test. A property on a base interface of SkipType, and on an interface implemented by a class SkipType, → existing CMP0057 (regression tests). No CMP0050 and no CS8926 anywhere. (b) Required test corrections (review F2/F3): a row DisableParallelization = false overrides an attribute DisableParallelization = true (merged false); real-runner tests in ComposeDataEmissionTests for a skip property inherited from the test class's base class, for a SkipType whose property is inherited from its own base class, and for a plain [Obsolete] skip property (compiled under the project's CS0612/CS0618 warnings-as-errors) | G1–G5 |
| S6 | B5.3 generic bases: GenericBase<T> with MemberType to a non-generic holder and with a member on a non-generic Root base; two closed subclasses (<int>, <string>) each composing with its own closed root (assert CreateRow's type through a custom provider seeing the closed request.DeclaringType, and {T} = typeof(GenericBase<int>).FullName + ".M" in an R0 failure); Two<,> and OuterPlain.InnerGen<> index registration; inaccessible nested test class → nothing emitted | G1–G5 |
| S7 | Completion: the snapshot matrix (each form × each member shape × [Shared]/no [Shared] × typed/untyped × metadata/no metadata × generic/non-generic); a Native AOT publish of test/Compono.XunitV3.Aot.Tests (-f net10.0, host RID, PublishAot=true) with 0 IL/AOT warnings, then run the published binary and confirm the ComposeData tests executed and passed (count them). If the project can't be AOT-published for a reason unrelated to B5, record the exact error and stop for the design owner; don't mark it passed. Then the B5 acceptance checklist. Amended by the S7 infrastructure decision (2026-11-09, Notes; product-owner approved): a global -p:PublishAot=true can't publish this project (NETSDK1207, unrelated to B5), so PublishAot=true is set inside the project through the project-scoped opt-in ComponoAotTestsPublish. The required command is dotnet publish test/Compono.XunitV3.Aot.Tests -c Release -f net10.0 -p:RuntimeIdentifier=osx-arm64 -p:SelfContained=true -p:ComponoAotTestsPublish=true -o <out>, followed by running <out>/Compono.XunitV3.Aot.Tests. Under that opt-in only, PublicApiSurfaceTests.cs is excluded from compilation, and ComposeDataAttributeTests is made trimming-safe with unchanged assertions. The gate itself is unchanged: 0 IL/AOT warnings, real execution of the published native executable, and a counted, passing ComposeData run. Host RID (osx-arm64) only; linux-x64 and CI integration are not part of S7 | G1–G5, plus the Native AOT result |
B5 acceptance checklist
- ADR-0070 Amendment 4 approved in principle by the product owner (2026-11-06, Notes)
- Product owner confirmed the narrowed Amendment 4 §2e before S0 (2026-11-06, Notes)
- #166 checkpoint re-checked at S2 start and recorded (B5.1)
- S0:
TConfigidentity fix; the B5.4 diagnostics at the B5.4 precedence; model fields; every B4 test and snapshot unchanged - S1: the four helper members with exact contracts; JIT switched over with its suite unchanged; AOT
PublicApiSurfaceTestsand ApiCompat additive - S2–S3 delivered per B5.6–B5.9, each slice gate-passed (G1–G5) with evidence in Notes (S2
bb67950, S30680dc2) - S4 delivered per B5.6–B5.9, gate-passed (G1–G5) and independently approved with evidence in Notes (S4
3fcc5d4) - S5 delivered per B5.6–B5.9, gate-passed (G1–G5) and independently approved with evidence in Notes (S5
07ff636f) - S6 delivered per B5.6–B5.9, gate-passed (G1–G5) and independently approved with evidence in Notes (S6
20c1eb9) - S7 delivered per B5.11: project-scoped Native AOT opt-in (
ComponoAotTestsPublish), AOT-onlyPublicApiSurfaceTestsexclusion, trim-safeComposeDataAttributeTests; gate-passed (G1–G5) and independently approved with evidence in Notes (S7faa09a4) - Emission only for zero-diagnostic, accessible-test-class models;
CMP0050isolation per method (S2 wiring, B5.6) - Member called through
MemberContainingTypeFqn;{M}/{T}fromtestClass/typeof(<Lookup>).FullName; no typed row element type named (final acceptance review, Notes 2026-11-09) - No reflection other than
ResolveGenericTestClassType's type-identity walk; no LINQ; noDataAttributeRegistration.CreateDataRow(final acceptance review, Notes 2026-11-09) - AOT
[Compose]template, emitter, discovery, and snapshots byte-identical (git blob-hash comparison againste709228; final acceptance review, Notes 2026-11-09) - S7 Native AOT publish: 0 IL/AOT warnings and
ComposeDatatests executed (144/144 native; 0 IL/AOT; Notes, 2026-11-09 S7 checkpoint) -
AnalyzerReleases.Unshipped.mdnotes updated for the newCMP0047/CMP0052/CMP0053/CMP0054conditions (theCMP0052note also covers the S5 static-interface skip property, ADR-0070 Amendment 4 §7) - S5: an active
static virtual/static abstractinterface skip property reports theCMP0052twin (never generatedCS8926); plain static interface skip properties emit and run; base-interface and implementing-class holders keepCMP0057(ADR-0070 Amendment 4 §7) - If valid skip metadata can't reach full parity, the work stopped and went to the product owner. There's no pre-approved fallback (unchanged from round 2)
B6 (compono-lsx.6, after B5 and B3): AOT real-runner tests, samples, Native AOT¶
B6 decisions (product owner, 2026-11-09; Notes):
- Q1, theory-level skip parity: approved ADR-0070 Amendment 5: normalize the AOT skip group into the theory's condition field. The always-false-
SkipUnlessproposal is rejected. Implemented by B6.0 below, before the rest of B6's skip-parity work. - Q2, AOT behavioral matrix:
ComposeDataTests.csadds only coverage missing beyond B5's existing tests (ComposeDataEmissionTests,ComposeDataRowsTests), audited item by item against B3's matrix. No wholesale duplication. - Q3, V7: the AOT consumer runs on the managed xUnit AOT runner. V3 is the Native AOT execution proof.
- Q4, CI: the existing linux-x64
aot-xunitv3-aot-pipelinejob, which publishes and runsCompono.XunitV3.Aot.SampleTests, is accepted as the CI gate for the new ComposeData samples. No workflow change. - B7 follow-up (from B6.0):
docs/packages/compono-xunitv3-aot.mddocuments that theory-levelSkipUnless/SkipWhenare read from[Fact]/[Theory]named arguments at compile time (ADR-0070 Amendment 5 §3), and that other test-attribute types aren't considered.
B6.0 AOT skip-group normalization (ADR-0070 Amendment 5)¶
Status: Complete (2026-11-09; implemented at 86bd7cb57a172c65fe39886f1f0db2a268934559, independently reviewed and approved, committed and pushed to origin/feat/163-composedata). B6's remaining items below (B6.1–B6.5) stay outstanding; B6 overall stays in progress.
A separately reviewed implementation slice: one commit, an independent review, then B6's remaining items.
Files changed (implementation commit 86bd7cb, 17 files, +2182 −4):
- Modified (9):
src/Compono.Generators/Discovery/AotComposeDataMethodDiscovery.cs,src/Compono.Generators/Emitters/AotComposeDataRegistrationEmitter.cs,src/Compono.Generators/Models/AotComposeDataMethodInfo.cs,src/Compono.Generators/Templates/AotComposeDataRegistration.scriban,src/Shared/Compono.XunitV3/ComposeDataRows.cs,test/Compono.Generators.Tests/AotComposeDataModelTests.cs,test/Compono.Generators.Tests/AotComposeDataRegistrationVerifyTests.cs,test/Compono.Generators.Tests/ComposeDataAotStandIns.cs,test/Compono.XunitV3.Aot.Tests/ComposeDataRowsTests.cs - New (8):
scripts/validate-theory-skip-parity.sh;test/Compono.XunitV3.Aot.Tests/ComposeDataTheorySkipParityTests.cs;test/Compono.XunitV3.Tests/ComposeDataTheorySkipMetadataTests.cs;test/Compono.XunitV3.Tests/ComposeDataTheorySkipParityTests.cs; and fourtest/Compono.Generators.Tests/Snapshots/verified snapshots (AotComposeDataRegistrationVerifyTests.TheoryLevelSkipUnless_RendersTheorySkipConditionand…TheoryLevelSkipWhen_RendersTheorySkipCondition, each with theAotComposeDataRegistrationandRowInvokerRegistrationoutputs).
Contract (exact):
- Discovery (
AotComposeDataMethodDiscovery). Read the test method's[Fact]/[Theory]attribute (Xunit.FactAttribute/Xunit.TheoryAttribute, both sealed inxunit.v3.core.aot) from compile-timeAttributeData. Record the theory's skip-condition kind on the model: - None: neither
SkipUnlessnorSkipWhenis a non-null string named argument, or both are; - Unless: only
SkipUnlessis; - When: only
SkipWhenis.
No runtime reflection. The generator may use an internal enum or named constants for the kind. - Both set: recorded as None, so xUnit's own validation is untouched. Scratch-verified: xUnit's analyzer rejects it at compile time on both engines (error xUnit1055), with or without Compono; with xUnit1055 suppressed, xUnit's AOT generator emits no test for the method before and after the change (0 discovered), and JIT runs it and reports the mutual-exclusion failure. Compono adds no rule. - Supported boundary: only the named arguments of [Fact]/[Theory] themselves. On AOT both are sealed (CS0509, probed), so constructor-set conditions in a derived test attribute can't be written. Any other test attribute (for example, one deriving from Xunit.Internal.FactAttributeBase) is treated as None, with no runtime reflection and no extra scope. B7 documents the boundary (B7 follow-up below). - Model. Add the kind to AotComposeDataAttributeMetadata (equatable, so incremental caching stays correct). Default None. - Emission. The template renders TheorySkipCondition = <value>, in the ComposeDataAttributeMetadata initializer only when the kind isn't None. Generated code for every theory without a condition, and therefore every existing snapshot, stays byte-identical. - Runtime (src/Shared/Compono.XunitV3/ComposeDataRows.cs, #if COMPONO_XUNITV3_AOT only). - ComposeDataAttributeMetadata gains one init property, TheorySkipCondition, public with [EditorBrowsable(EditorBrowsableState.Never)], default 0. It's an int code: 0 = None, 1 = Unless, 2 = When, documented in its XML doc. No public enum and no new public type. Generated code emits the integer value. - The runtime compares the code only against named internal constants (for example TheorySkipConditionNone/Unless/When on ComposeDataRows), never bare literals. - Invalid code. When normalization applies (effective Skip non-null and code ≠ 0) and the code isn't 1 or 2, throw InvalidOperationException with the exact message ComposeData received an unsupported theory skip-condition code {code}; expected 0 (None), 1 (SkipUnless), or 2 (SkipWhen). An unknown code is never treated as None, Unless, or When. When the effective Skip is null, the code isn't consulted. - In CreateDataRow, after the existing grouped row-versus-attribute skip selection: if the effective Skip is non-null and TheorySkipCondition isn't None, rewrite the group into the theory's field: - Unless: SkipUnless = unless ?? (when is null ? AlwaysFalse : () => !when()); SkipWhen = null. - When: SkipWhen = when ?? (unless is null ? AlwaysTrue : () => !unless()); SkipUnless = null. - AlwaysFalse/AlwaysTrue are cached static readonly Func<bool> fields holding static lambdas. A negating closure is allocated only for an opposite-kind conditional predicate. Skip (the reason) is never changed. - When TheorySkipCondition is 0 (None), or the effective Skip is null, the row is exactly what B5.8 produces. - The JIT branch is unchanged. - Prohibited: reflection, dynamic invocation, new runtime dependencies, any public type, and any generated-code change beyond the conditional line.
Tasks:
- Discovery and model: the theory skip-condition kind, with generator tests for
[Theory]with no condition, withSkipUnlessonly, withSkipWhenonly, with both (recorded None, no Compono diagnostic),[Fact], and a theory that also carries[InlineData] - Emission: conditional
TheorySkipConditionrendering; one new Verify snapshot for each non-None kind; 0 changes to existing snapshots - Runtime normalization per the contract, with full XML docs
-
ComposeDataRowsTests(AOT): unit tests for both kinds × {unconditional, same-kind, opposite-kind true/false} groups, asserting the resultingSkipUnless/SkipWhenpair and the preservedSkip; the None and null-Skipno-op cases; delegate caching for unconditional groups; an invalid code (for example 3 and -1) with an effectiveSkipthrowing the exactInvalidOperationExceptionmessage; and an invalid code with no effectiveSkipnot consulted - Full parity matrix, real runners, mirrored in
Compono.XunitV3.TestsandCompono.XunitV3.Aot.Tests: theory {none,Skip+SkipUnlesstrue/false,Skip+SkipWhentrue/false, unconditionalSkip} × ComposeData {no skip; rowSkipunconditional /SkipUnlesstrue/false /SkipWhentrue/false; attributeSkipunconditional / each condition kind and value; row-versus-attribute precedence: unconditional row over conditional attribute, conditional row over unconditional attribute, opposite-kind row over attribute}: 84 cases. Assert identical run/skip outcomes and skip reasons on both engines. This satisfies B6's theory-level interaction item below - Gates: G1 full build 0/0; G2 full suite green; generator tests on net10.0/net11.0 with no unexpected
CMP0050; the AOT real-runner suite on net9.0/net10.0/net11.0; and the S7 Native AOT publish oftest/Compono.XunitV3.Aot.Tests(-p:ComponoAotTestsPublish=true, net10.0, host RID) with 0 IL/AOT warnings and the parity matrix executed and counted in the native run
B6.1–B6.5 Remaining slices¶
Status: B6.1 is complete (2026-11-10; implemented at 8cd80cbef396d2364d93fb4b8bf02e24c3d62809, independently reviewed by Claude Opus and approved with no blocking findings, committed and pushed to origin/feat/163-composedata; Notes, "B6.1 completion checkpoint"). B6.2–B6.5 are not started; B6 overall stays in progress. This section formalizes the slicing approved by the product owner on 2026-11-09 (Notes, "B6.1–B6.5 slicing"). B6.0 above, and B5's acceptance record, are unchanged by it. 2026-11-10: the remaining B6.2–B6.5 scope is reduced and regrouped into Passes 1–2 by "Streamlined remaining work" below (product owner; ADR-0070 Amendment 6). The B6.1 record, SQ2, SQ4, SQ6, and SQ7 still apply. SQ1's five-slice structure, SQ3's disposal wording, and SQ5's cite-before-adding rule are carried into the passes as described there.
Slicing decisions (product owner, 2026-11-09). Numbered SQ1–SQ7 so they aren't confused with the B6 decisions Q1–Q4 at the top of this section, which still stand.
- SQ1, slices: approved. B6.1 cross-engine text and name parity; B6.2 AOT behavioral-matrix gap fill; B6.3 metadata and skip real-runner parity; B6.4 generic bases and Amendment 4 JIT side; B6.5 samples and packaged validation.
- SQ2, literal parity: where a literal cross-engine comparison is needed, the JIT and AOT parity fixtures use identical namespaces (and identical type and member names), so the expected strings are identical literals in both projects. R0 messages and seed lines are asserted with exact string equality (no
Contains, wildcard, or regex). For{M}/{T}: verify the same formatting rule on both engines; assert literal equality wherever the type identity is genuinely identical; account explicitly for TFM-dependentType.FullNamevalues (for example, a closed generic's assembly-qualified type arguments carry the runtime assembly version); never normalize away a meaningful naming difference; and add no shared test-framework abstraction (no linked expectations file, no helper library) solely for parity. - SQ3, disposal: AOT disposal is verified through the B5.10 harness; JIT gets a documenting test. No order-dependent real-runner disposal assertions.
- SQ4, cancellation: real-token cancellation is deferred. The S4 known limitation (Notes, S4) stays documented as is; B6.2 doesn't implement it.
- SQ5, existing coverage: existing skip and
DisableParallelizationcoverage is audited. A requirement is marked complete only when existing tests actually prove the required behavior, with the proving tests cited. B5/B6.0 coverage isn't duplicated to raise test counts. - SQ6, B6.2 audit: B6.2 records an item-by-item B3-to-AOT coverage audit in Notes.
- SQ7, V7: V7 uses an independent scratch consumer outside the repository, with local-feed
PackageReferencedependencies. The in-repository*SampleTestsprojects don't substitute for V7.
Remaining checklist (each item has exactly one owner slice, or is marked a cross-slice rule):
- [Pass 1, core] ADR-0070 Amendment 6 core fix in
CompositionBuilder.AddProfile<TProfile>(), with its regression tests and acceptance criteria (Pass 1, "Core fix"). Satisfied by Pass 1 (uncommitted pending review; Notes, "Pass 1 completion checkpoint", 2026-11-10): the guardedCreateProfile<TProfile>()step is the onlysrc/change, with the three core regression tests and the arity-1 ComposeData parity tests on both engines - [Pass 1, B6.2]
test/Compono.XunitV3.Aot.Tests/ComposeDataTests.cs: the reduced behavioral set in Pass 1 (plainAssert). It covers fixtures, parallel isolation,[InlineData]mixing, the execution stage, and the disposal ownership boundary. Assembly fixtures are excluded with a comment citing xunit/xunit#3664. (Was: "the same behavioral matrix as B3". Reduced 2026-11-10: B3 rows already proven by existing tests are cited in the Notes audit table, not re-tested.) Satisfied by Pass 1 (uncommitted pending review; Notes, "Pass 1 completion checkpoint", 2026-11-10) - [B6.1] Cross-engine message parity: a test in each project asserts the exact R0 text for the same failing scenario. Satisfied by B6.1 (implementation commit
8cd80cbef396d2364d93fb4b8bf02e24c3d62809): one exact case per runtime R0 failure class (null row, unsupported row shape, too many values, null for a non-nullable parameter, type mismatch,TConfigextraction, member read throws, profile constructor throws), asserted with exact string equality and the same literal in both files (ComposeDataParityTestsin each project; Notes, B6.1 completion checkpoint) - [Pass 1, B6.3] Cross-engine metadata parity: the same metadata matrix as B3 (skip conditions with and without
SkipType, grouped precedence, explicit, timeout, traits, labels) yields the same skipped/run/explicit outcomes and display names on AOT. Reduced 2026-11-10: satisfied by citing existing evidence (Pass 1, "B6.3 evidence"), plus one AOT real-runner row/attributeExplicittheory. NoSkipTypereal-runner theory is added unless the Pass 1 citation review finds an unproven link. Satisfied by Pass 1 (uncommitted pending review; Notes, "Pass 1 completion checkpoint", 2026-11-10): the one addedExplicittheory isComposeDataExplicitTestsin the newComposeDataTests.cs. - [Pass 1, B6.3, by citation] Real-runner skip parity: mirror the JIT baseline B3 established in
test/Compono.XunitV3.Tests/ComposeDataTheoryTests.cs(ComposeDataRowSkipConditionTests,ComposeDataAttributeSkipTypeTests,ComposeDataSkipPrecedenceTests,ComposeDataTheoryLevelSkipTests) — row-level and attribute-levelSkipUnless/SkipWhenwithSkipTypeon a different class, grouped-skip precedence (unconditional rowSkipwins), and theory-level[Theory(Skip = …)]conditions combined with unconditional rowSkip— and assert identical run/skip outcomes and skip reasons on JIT and AOT. Satisfied by citation (Pass 1, 2026-11-10): the B6.0ComposeDataTheorySkipMatrix_*suite runs the full 84-case matrix on both engines (validate-theory-skip-parity.sh84/84 PASS on JIT net10.0 and on the S7 Native AOT binary), and the AOT S5/jit baseline tests cited under "B6.3 evidence" cover the rest (uncommitted pending review). - [B6.0, satisfied] Theory-level
SkipUnless/SkipWheninteraction tests (RESEARCH-0038 §2.6 item 3, §9.2 S2), mirrored in both test projects: a[Theory(Skip = …, SkipUnless = …)]and a[Theory(Skip = …, SkipWhen = …)](condition true and false), each combined with (a) noComposeDataskip, (b) a rowSkipwith no condition, © a rowSkipwith its own condition, (d) an attributeSkipwith and without a condition. Assert identical run/skip outcomes and skip reasons on JIT and AOT. If the planned approach can't reach parity, stop and request review. No speculative workarounds. Satisfied by B6.0 (implementation commit86bd7cb57a172c65fe39886f1f0db2a268934559): the 84-case real-runner parity matrix (test/Compono.XunitV3.Tests/ComposeDataTheorySkipParityTests.cs,test/Compono.XunitV3.Aot.Tests/ComposeDataTheorySkipParityTests.cs) is a superset of this item's (a)–(d) combinations, plus an unconditional theorySkip, and it passed 84/84 with matching outcomes and skip reasons on JIT (net9.0/net10.0/net11.0), the managed AOT runner (net9.0/net10.0/net11.0), and the real Native AOT binary (net10.0) (Notes, B6.0 completion checkpoint) - [B6.1] Seed parity: the same seed-line variants as B3, with identical text. Satisfied by B6.1 (implementation commit
8cd80cbef396d2364d93fb4b8bf02e24c3d62809): not assigned (via the null-row scenario), attribute seed, attribute seed0, profileWithSeed, negative profile seed, and the duplicate-seed conflict, with mirrored fixtures so both engines assert identical literals (Notes, B6.1 completion checkpoint) - [B6.1] Name parity:
typeof(…).FullName-based{M}/{T}text matches JIT under JIT-run and real Native AOT, for a nested test class and a closed-generic holder. Inherited test methods withoutMemberTypeare excluded from{M}byte parity (C-1: JIT names the concrete class, AOT the declaring class); add one test per engine that documents that difference. Satisfied by B6.1 (implementation commit8cd80cbef396d2364d93fb4b8bf02e24c3d62809): the nested test class pins the CLR+form and the AOT registration-index.form as literals; the closed-generic holder takes only the TFM-dependent assembly-qualified type-argument portion fromtypeof(…).FullNameand pins the namespace/name/arity/brackets as literals; names are never normalized or stripped; verified under JIT, the managed AOT runner, and the real Native AOT binary; one C-1 documenting test per engine (Notes, B6.1 completion checkpoint) - [B6.1] ADR-0070 Amendment 2 documenting tests (runtime side; the AOT compile-time side is B4's
CMP0051/CMP0044tests): (a) an inherited test method with a member that a derived class hides, withoutMemberType, asserting each engine's actual resolution (JIT: the derived member; AOT: the declaring-class member); (b) the same inherited test with an explicitMemberType, asserting identical rows on both engines; © on JIT only, a private nestedMemberTypeholder that runs (AOT can't compile it,CMP0044). Each test carries a comment naming Amendment 2. Satisfied by B6.1 (implementation commit8cd80cbef396d2364d93fb4b8bf02e24c3d62809): all three documenting tests are present, with ©'s holder a genuinely private nested class exercised through the JIT reflection path and no AOT counterpart (Notes, B6.1 completion checkpoint) - [Pass 2, B6.4 reduced] Generic test-class bases (ADR-0070 Amendment 4 §1), JIT side: one JIT real-runner test. An inherited
[ComposeData]theory on a generic base (withMemberTypeto a non-generic holder) runs through two closed subclasses (<int>,<string>), and asserts the rows and that a customICompositionValueProvidersees the closedrequest.DeclaringType, the same assertion as the AOT S6 tests (ComposeDataEmissionTestsIntGenericComposeTests/StringGenericComposeTests). (Was: "mirrored in both projects" plus{T}R0 text. Reduced 2026-11-10: the AOT side is already B5 S6, and closed-generic{M}/{T}formatting is already B6.1 name parity.) Satisfied by Pass 2 (uncommitted pending review; Notes, "Pass 2 completion checkpoint", 2026-11-10):ComposeDataGenericBase<T>plusComposeDataGenericIntTests/ComposeDataGenericStringTestsintest/Compono.XunitV3.Tests/ComposeDataTheoryTests.cs; four real-runner cases assert the closed base is the composition root; no production defect found (JITtestMethod.DeclaringTypeis the closed base as-is). - Dropped (product owner, 2026-11-10), formerly [B6.4]: ADR-0070 Amendment 4 §2 documenting tests. They would only show that JIT reflection accepts shapes AOT rejects. The AOT rejections are already generator-tested (B5 S0/S5), and no JIT behavior depends on them. The
int+stringcross-engine parity test is optional (AOTStaticMixedSource_ReadsReferenceRowsInOrder/AsyncMixedReferenceAndValue_TakesTheReferenceReaderAtRuntimealready cover the AOT side), and is added only if Pass 2 shows a real gap. Original text, kept for traceability: ADR-0070 Amendment 4 §2 documenting tests (runtime side, JIT project only, each with a comment naming Amendment 4; the AOT compile-time side is covered by B5 S0): a member declared onGenericBase<T>withoutMemberTyperuns on JIT; a static virtual interface member through an interfaceMemberTyperuns on JIT; an[Obsolete(error: true)]data member runs on JIT; a member type implementingIAsyncEnumerable<string>andIAsyncEnumerable<Uri>, and one implementingIAsyncEnumerable<int>andIAsyncEnumerable<long>, each run on JIT (they'reCMP0053on AOT); an activeSkipUnlessnaming astatic virtualinterface property throughSkipType = typeof(IFoo)runs on JIT with its condition honored (it'sCMP0052on AOT, Amendment 4 §7a); plus a cross-engine parity test for the supportedint+stringshape (both engines read thestringrows) - [Pass 1, B6.3, by citation]
DisableParallelizationparity: absent,true, and a row override yield the same mergedITheoryDataRow.DisableParallelizationon both engines (B5.8). Expected to be satisfied by the existing tests listed under Pass 1 "B6.3 evidence"; no new test unless that review finds a gap. Satisfied by citation (Pass 1, 2026-11-10): existing JIT/AOT tests cited under "B6.3 evidence" (uncommitted pending review). - [Cross-pass rule, Passes 1–2] Reuse B5.10's
RegisteredComposeDataFactoryharness for AOT failure-path, metadata, and disposal assertions. Don't add a second harness. The only permitted harness change is Pass 1's optionalDisposalTrackerparameter. Ticked when Pass 2 closes, if both reviews confirmed it. Confirmed by Pass 2: no new AOT test and no harness change in Pass 2 (B6.4 is JIT-only; B6.5 changes only sample projects), so the rule holds with Pass 1's single permitted change. - [Pass 2, B6.5] Samples:
test/Compono.XunitV3.SampleTests/ComposeDataTests.csandtest/Compono.XunitV3.Aot.SampleTests/ComposeDataTests.cs, documentation-quality, the same three examples in each: the per-rowTConfigpricing table, a[Shared]dependency, and a fixture-backed theory. An async source is optional. Samples don't duplicate test-suite coverage. Satisfied by Pass 2 (uncommitted pending review; Notes, "Pass 2 completion checkpoint", 2026-11-10): both new files carry the same three examples; no async source added (optional); no redundant variants. - Moved to Pass 4 (B9), 2026-11-10, formerly [B6.5]: V3, the real Native AOT publish and run of
Compono.XunitV3.Aot.SampleTests(0 IL/AOT warnings, allComposeDatatests executed and counted), and V7 pack-and-consume (SQ7). Each runs once, on the final tree, in B9; they're no longer repeated in B6
Common slice rules (B6.1; superseded for the remaining work by "Streamlined remaining work" below):
- One slice at a time: implement, gate, independent review, then product-owner approval before commit. The next slice starts only after the previous one is committed and recorded in Notes.
- Tests and samples only. Each slice changes no
src/file, generator template, Verify snapshot, ADR, or CI workflow. If a slice finds that production code must change to reach parity, it stops and reports to the design owner. No fix inside the slice. - The B5.10 harness rule above applies to every AOT failure-path and metadata assertion. Failure and metadata targets are plain
[ComposeData…]methods without[Theory](B5.10's approved S2 deviation). - No duplication of B5/B6.0 coverage (Q2, SQ5). A requirement already proven by an existing test is ticked with that test cited, not re-tested.
- Common gates for every slice: G1 (full build 0/0) and G2 (full suite green, no modified
*.verified.*,git diff --stat -- src/empty), plus the JIT suite and the managed AOT suite on net9.0/net10.0/net11.0, and no regression in the B6.0 parity validator (scripts/validate-theory-skip-parity.sh, 84/84 on at least JIT net10.0). Slices that add AOT tests also rerun the S7 Native AOT publish oftest/Compono.XunitV3.Aot.Tests(-p:ComponoAotTestsPublish=true, net10.0, host RID) with 0 IL/AOT warnings and the new tests counted in the native run.
B6.1: cross-engine text and name parity. Owns: message parity, seed parity, name parity (including the C-1 documenting tests), and the Amendment 2 documenting tests.
- Scope. End-to-end failure scenarios run the same way on each engine: JIT through the real attribute's
GetData(not only the helper), AOT through the harness. Cover one scenario per runtime R0 failure class reachable on both engines (null row, unsupported row shape, too many values, null for a non-nullable parameter, type mismatch,TConfigextraction failure, member read throws, profile constructor throws), and each seed-line variant B3 defines (not assigned, attribute seed, attribute seed0, profileWithSeed, negative profile seed, the duplicate-seed conflict). Failures that are compile-timeCMPdiagnostics on AOT (for example D1–D5) aren't runtime parity scenarios. Name parity covers a nested test class and a closed-genericMemberTypeholder, under JIT, the managed AOT runner, and the real Native AOT binary. The C-1 test (one per engine) and the Amendment 2 tests (a)–© each carry a comment naming their decision. - Assertions (SQ2). The parity fixtures sit in an identical namespace in both projects. R0 and seed-line text is asserted exactly, with the same literal in both files wherever the type identity is identical. For a closed generic, whose
FullNameincludes assembly-qualified type arguments that vary by TFM, the test asserts the shared formatting rule ({FullName}.{Member}built fromtypeof(…).FullName) with the type-name part taken fromtypeof(…).FullNamein that run, and records the rule in a comment. It doesn't strip or rewrite the name. - Expected files. New:
test/Compono.XunitV3.Tests/ComposeDataParityTests.csandtest/Compono.XunitV3.Aot.Tests/ComposeDataParityTests.cs. Modified: this plan (checkboxes and a Notes checkpoint) only. No other file is expected; any other file needs a stated reason in the slice report. - Gates. The common gates, including the Native AOT publish with the B6.1 tests counted in the native run. Record the parity scenario list (each one paired across the two files) and the per-project, per-TFM test-count change in Notes.
- Stop conditions. Stop and report, without working around it, if: any R0 or seed-line text differs between engines for the same scenario;
{M}/{T}differ under real Native AOT from the managed run for a genuinely identical type; a scenario can't be reached through the harness without a second harness or asrc/change; or a test would contradict ADR-0070 (including Amendment 2's resolution rules).
B6.2–B6.5 (original slice specifications): superseded on 2026-11-10 by "Streamlined remaining work" below. The original texts were plans, not completed work, and are replaced rather than kept as stale requirements. The decision trail is in Notes ("B6.1–B6.5 slicing", 2026-11-09, and "B6.2 audit and streamlining", 2026-11-10).
Streamlined remaining work (product owner, 2026-11-10; ADR-0070 Amendment 6)¶
Decisions (D1–D7). Numbered separately from Q1–Q4 and SQ1–SQ7, which stand except where noted.
- D1, core fix: ADR-0070 Amendment 6 is approved and lands in #163, on this branch and in this PR. There's no separate PR.
- D2, B6.2: reduced to the set in Pass 1. B3 rows already proven by existing tests are cited in the Notes audit table, not re-tested. This refines SQ5/SQ6: the audit is recorded, but tests aren't added just to fill every historical B3 row.
- D3, B6.3 and B6.4: B6.3 is satisfied by citing existing evidence, plus at most one AOT real-runner
Explicittheory and oneSkipTypetheory (theSkipTypetheory only if needed; see Pass 1). The 84-case matrix isn't duplicated. B6.4 keeps one JIT generic-base test; the six Amendment 4 §2 JIT documenting tests are dropped;int+stringparity is optional. - D4, B6.5 samples: three examples (the
TConfigpricing table, a[Shared]dependency, a fixture-backed theory). Async is optional. - D5, B7 + B8: one pass.
- D6, B9: comprehensive validation once, on the final tree. V2 is folded into V1; V5 and V7 run in one scratch consumer; V3 and V7 aren't repeated earlier; V9 is a regression signal with no new rigid threshold.
- D7, workflow: four passes. Each pass is one cohesive implementation, one independent review, and one commit containing both the implementation and its plan checkpoint. There's no separate documentation-closeout commit, and committing still needs product-owner approval. The B5, B6.0, and B6.1 records are history and aren't rewritten.
Rules for every pass:
- Scope. Pass 1 may change exactly one
src/file,src/Compono/CompositionBuilder.cs(Amendment 6), including its XML documentation where necessary. Pass 1 does not regeneratedocs/reference/api; Pass 3 owns all API-reference regeneration (V4) and strict docs validation. That deferral is safe because thedocs.ymlAPI-reference drift check runs only on pull requests and pushes tomain, and the #163 PR opens only after Pass 4. No other pass changessrc/, a generator template or emitter, a Verify snapshot, an ADR, a CI workflow, or a project file. If a pass finds a production change is needed (for example, an AOT runner behavior that contradicts ADR-0070), it stops and reports to the design owner. No fix inside the pass. - Harness. AOT failure-path, metadata, and disposal assertions reuse B5.10's
RegisteredComposeDataFactory; targets are plain[ComposeData…]methods without[Theory]. The only permitted harness change is Pass 1's optionalDisposalTrackerparameter. - Common gates.
- G1: full build, 0 warnings and 0 errors.
- G2: full suite green, which covers JIT on net8.0–net11.0, the managed AOT runner on net9.0–net11.0, generators, core, and the samples. No modified
*.verified.*files. - A pass that adds or changes AOT tests reruns the S7 Native AOT publish of
test/Compono.XunitV3.Aot.Tests(-p:ComponoAotTestsPublish=true, net10.0, host RID): 0 IL/AOT warnings, and the new tests executed and counted in the native run. - The B6.0 skip-parity validator reruns only in a pass that touches skip or metadata code (none is planned), and in Pass 4.
Pass 1: core fix + B6.2/B6.3 behavioral coverage¶
Core fix (ADR-0070 Amendment 6). In CompositionBuilder.AddProfile<TProfile>(), construct the profile in a separate guarded step (for example, a private static CreateProfile<TProfile>()), then call the unchanged ApplyProfile. The guard catches only a TargetInvocationException with a non-null InnerException thrown by new TProfile(), and rethrows the inner exception with ExceptionDispatchInfo.Capture(inner).Throw().
Acceptance criteria:
- The original constructor exception reaches the caller as the same instance: type, message,
InnerExceptionchain,CompositionException.Diagnostic, and the original stack-trace frames are preserved. Satisfied by Pass 1 (uncommitted pending review; Notes, "Pass 1 completion checkpoint", 2026-11-10). - Exceptions from
Configure, including aTargetInvocationExceptionthrown inside it, propagate exactly as before. Cycle detection is unchanged. Satisfied by Pass 1 (uncommitted pending review; Notes, "Pass 1 completion checkpoint", 2026-11-10). - A
TargetInvocationExceptionwith a null inner exception propagates unchanged. Satisfied by Pass 1 (uncommitted pending review; Notes, "Pass 1 completion checkpoint", 2026-11-10). - No public API change: signatures, types, and members are all unchanged.
AddProfile(ICompositionProfile)is unchanged. There's no reflection, dynamic code, or new dependency. Satisfied by Pass 1 (uncommitted pending review; Notes, "Pass 1 completion checkpoint", 2026-11-10). - Where needed, the XML docs for
AddProfile<TProfile>()state that a constructor's exception propagates unchanged. No API-reference regeneration in Pass 1: Pass 3's V4 regeneratesdocs/reference/apiforComponoalong with the ComposeData packages. Satisfied by Pass 1 (uncommitted pending review; Notes, "Pass 1 completion checkpoint", 2026-11-10). - Core tests,
test/Compono.Tests/ComposerProfileTests.cs(additions only): Satisfied by Pass 1 (uncommitted pending review; Notes, "Pass 1 completion checkpoint", 2026-11-10). - A constructor throwing
InvalidOperationExceptionreaches the caller as the same instance (Assert.Same), and its stack trace contains the constructor frame. - A constructor throwing a
CompositionExceptionwith aDiagnosticreaches the caller as the same instance, with the sameDiagnosticreference. - A
TargetInvocationExceptionthrown fromConfigureis not unwrapped: the same instance reaches the caller. - ComposeData arity-1 parity, in both
ComposeDataParityTests.csfiles. JIT goes through the realGetData; AOT goes through the harness, with targets in the identicalCompono.XunitV3.ComposedDataParitynamespace (SQ2). Satisfied by Pass 1 (uncommitted pending review; Notes, "Pass 1 completion checkpoint", 2026-11-10). - A throwing parameterless constructor: the exact R0 text
ComposeData row 0 from '{M}' for '{T}' failed: <constructor message>\n\nSeed: not assigned (the failure occurred before the row's composition started), with the same literal on both engines, andInnerExceptionis the original exception type (notTargetInvocationException). - A constructor throwing a
CompositionExceptionwith aDiagnostic: the R0Diagnosticis the same reference, andInnerExceptionis the constructor's exception. - Existing
Composeregression: the full suite stays green with no test edits. EveryCompose<TProfile>package (xUnit v3, TUnit, NUnit, MSTest) and the AOT[Compose<P>]registration reach the fix through the same core method. No per-framework tests are added. Satisfied by Pass 1 (uncommitted pending review; Notes, "Pass 1 completion checkpoint", 2026-11-10): full suite total 6733 / failed 0. - Real Native AOT: tests 4 and 5 execute in the S7 native binary. Satisfied by Pass 1 (uncommitted pending review; Notes, "Pass 1 completion checkpoint", 2026-11-10): native run 275 total / 0 failed,
Profile_parameterless_constructor2/2.
B6.2 retained tests (test/Compono.XunitV3.Aot.Tests/ComposeDataTests.cs, new; reuse RequireFixtureIdFixture, FixtureId, and IdFixtureProfile from RequireFixtureTests.cs, so there's no new fixture or profile type for this group):
- Fixtures, parallel isolation, and execution stage (two real-runner classes): Satisfied by Pass 1 (uncommitted pending review; Notes, "Pass 1 completion checkpoint", 2026-11-10).
- Class A uses
IClassFixture<RequireFixtureIdFixture>. Class B uses[Collection]on one new[CollectionDefinition]withICollectionFixture<RequireFixtureIdFixture>. - Each has one
[Theory]+[ComposeData<IdFixtureProfile>(nameof(Rows))], with two rows. - Each class's
Rows()member readsTestContext.Current.RequireFixture<RequireFixtureIdFixture>().Idinto the row values and recordsTestContext.Current.PipelineStagein a static on its own class. - The theory asserts the row-supplied id equals the injected fixture's
Id, the composedFixtureIdequals it, and the recorded stage isTestPipelineStage.TestMethodExecution. - This covers class and collection fixtures, a fixture read inside the member, a fixture read through a profile, the execution stage, and isolation: the two classes run concurrently with distinct fixture instances. (Two classes, not four: decided 2026-11-10.)
- Mixed
[InlineData]and[ComposeData]: one theory in Class A with[ComposeData(nameof(MixedRows))]+[InlineData(99, "inline")]. The ComposeData rows bind their values, and the InlineData row passes through uncomposed, as in JITComposeDataMixedSourceTests. Satisfied by Pass 1 (uncommitted pending review; Notes, "Pass 1 completion checkpoint", 2026-11-10). - Disposal ownership boundary (harness). Satisfied by Pass 1 (uncommitted pending review; Notes, "Pass 1 completion checkpoint", 2026-11-10).
- One plain
[ComposeData<…>]target whose first parameter is supplied by the row (a new disposable instance per read) and whose second parameter of the same small test-localIDisposabletype is composed through a profile registration. - One
[Fact]invokes it with a caller-ownedDisposalTracker. It assertstracker.TrackedObjectsis empty, then callsawait tracker.DisposeAsync()and asserts neither value is disposed. - It needs the one harness change: an optional
DisposalTracker? disposalTracker = nullparameter onRegisteredComposeDataFactory.InvokeAsync, passed asdisposalTracker ?? new DisposalTracker(), with existing callers unchanged. - Assembly fixtures: a comment only, citing xunit/xunit#3664 (the AOT test project pins
xunit.v3.aot.mtp-v24.0.1). Revisit if the pin moves to a fixed release. Satisfied by Pass 1 (uncommitted pending review; Notes, "Pass 1 completion checkpoint", 2026-11-10). - JIT disposal documenting test (
test/Compono.XunitV3.Tests/ComposeDataGetDataTests.cs, addition): the same shape through the realGetData(method, tracker).TrackedObjectsstays empty, and nothing is disposed. A comment explains that the xUnit JIT runner itself registers theory arguments (DisposalTracker.AddRange(row.GetData()), RESEARCH-0038 §2.4), so JIT disposal is xUnit's behavior, not Compono's. No real-runner disposal assertion (SQ3). Satisfied by Pass 1 (uncommitted pending review; Notes, "Pass 1 completion checkpoint", 2026-11-10).
B6.3 evidence (each item is ticked by citation once the Pass 1 review confirms the cited tests prove it). Ticked by citation in Pass 1 (uncommitted pending review; Notes, "Pass 1 completion checkpoint", 2026-11-10):
- Skip conditions, grouped precedence, theory-level interaction. These are already proven by:
- the B6.0 84-case real-runner matrix in both
ComposeDataTheorySkipParityTests.csfiles (JIT, managed AOT, Native AOT), which shows the AOT runner honors row and attribute predicates and their precedence; - the AOT S5 harness tests in
ComposeDataEmissionTests(SkipType_Holder_BindsPredicateReturningTrue,SkipType_InheritsPropertyFromBaseClass_BindsPredicate,SkipProperty_OnTestClassBaseClass_BindsPredicate,RowSkip_WinsOverAttributeGroup,RowPredicate_WinsOverAttributePredicate,SkipUnless_*/SkipWhen_*), which show aSkipTypecondition binds to the right holder's property; - the JIT real-runner baseline in
ComposeDataTheoryTests.cs(ComposeDataRowSkipConditionTests,ComposeDataAttributeSkipTypeTests,ComposeDataSkipPrecedenceTests,ComposeDataTheoryLevelSkipTests).
Taken together, these prove AOT SkipType outcomes. Default: no SkipType real-runner theory. Add one only if the review finds the chain incomplete. (Pass 1 also re-ran scripts/validate-theory-skip-parity.sh: 84/84 PASS on JIT net10.0 and on the S7 Native AOT binary; no SkipType real-runner theory was added.) - Explicit. The AOT merge is harness-proven (MetadataRows_CarryConstantsAndSeedTrait), but no AOT real-runner test shows the runner honoring a factory row's Explicit. Add one AOT real-runner theory, [ComposeData(nameof(Rows), Explicit = true)], whose body calls Assert.Fail. A green run with the case reported as not run proves it. (Added in Pass 1: ComposeDataExplicitTests in test/Compono.XunitV3.Aot.Tests/ComposeDataTests.cs; the native run reports its 2 cases as skipped, body never runs.) - Timeout, traits, labels, display names. Cite AOT MetadataRows_CarryConstantsAndSeedTrait, Traits_AreUnionedWithRowAndSeed, and the JIT real-runner ComposeDataSkipAndExplicitTests and ComposeDataDisplayAndTraitTests. No new tests. (Ticked by citation in Pass 1; no new tests added.) - DisableParallelization. Cite JIT ComposeDataRowsTests.CreateDataRow_*DisableParallelization*, ComposeDataGetDataTests.GetData_AttributeDisableParallelization_IsMergedOntoRows and GetData_DisableParallelization_DefaultsToFalse, and AOT DisableParallelization_DefaultsToFalse, DisableParallelization_RowOverrideWins, and DisableParallelization_AttributeTrueRowFalse_RowWins. No new tests. (Ticked by citation in Pass 1; no new tests added.)
Not added (existing evidence or out of scope; see the Notes audit table): nullable-parameter binding, seed reproduction, a later labelled-row failure, the sync (T1,T2)[] and TheoryDataRow<T…>[] shapes, List<object?[]>, the extra two isolation classes, and real-token cancellation (SQ4, deferred).
Pass 1 gates: the common gates, plus:
git diff --stat -- src/lists onlysrc/Compono/CompositionBuilder.cs. (Confirmed in Pass 1: 1 file changed, +35/-2.)- The S7 Native AOT publish runs with the new tests counted. (Confirmed in Pass 1: native run 275 total / 0 failed, including the arity-1 parity, Class A/B, Explicit, and disposal tests.)
- The Notes checkpoint records the B3 audit table (or confirms the 2026-11-10 one), the per-project test-count change, the native run counts, and which B6.3 items are ticked by citation. (Confirmed in Pass 1: Notes, "Pass 1 completion checkpoint", 2026-11-10.)
Pass 1 stop conditions:
- The arity-1 R0 text differs between engines after the fix.
- The AOT runner doesn't honor row
Explicit, or the fixture, stage, or isolation assertions fail on AOT. [InlineData]+[ComposeData]doesn't run on AOT.- Any of these needs a
src/change other than Amendment 6's.
Pass 2: reduced B6.4 + B6.5 samples¶
- One JIT generic-base test, in
test/Compono.XunitV3.Tests/ComposeDataTheoryTests.cs(addition). It needs a small JIT probe provider, profile, and record, mirroring the AOTDeclaringTypeProbe*types. Done (Pass 2): probe provider/profile/recordComposeDataGenericDeclaringTypeProbe*; the base isComposeDataGenericBase<T> : ComposeDataGenericRoot, with a non-genericComposeDataGenericMemberHolder. - The generic base declares an inherited
[Theory]+[ComposeData<…ProbeProfile>(…, MemberType = typeof(NonGenericHolder))]. Done (two inherited theories:HolderRowsviaMemberType,RootRowsinherited from the non-generic root). - Two closed subclasses (
<int>,<string>) run it. Done (ComposeDataGenericIntTests,ComposeDataGenericStringTests). - It asserts the rows and that the probe provider saw the closed generic base as
request.DeclaringType, the same assertion as AOT S6. Done (four real-runner cases pass;probe.DeclaringType.Should().Be(GetType().BaseType)). - Samples: the three D4 examples in both
*SampleTests/ComposeDataTests.cs. Async is optional. Done (Pass 2): pricing-tableComposeData<TProfile,TConfig>,[Shared]row value reused by a composed service, and a fixture-backed row member; same three in both projects; no async source. - Optional, only if a real gap is shown: the
int+stringcross-engine parity test. Not needed — no gap was shown; the AOT side is already covered (StaticMixedSource_ReadsReferenceRowsInOrder/AsyncMixedReferenceAndValue_TakesTheReferenceReaderAtRuntime), and the JIT generic-base test exercises<string>on JIT. - Gates: the common gates. No AOT test changes, so there's no S7 rerun. The AOT samples run in G2 under the managed runner; their Native AOT execution is Pass 4 V3 plus the existing linux-x64 CI job (Q4).
Pass 3: B7 + B8 (documentation and skill, one pass)¶
Runs the B7 and B8 checklists below as one implementation, with one review and one commit. That includes the D5 additions recorded in those checklists (the Amendment 6 release note, and a small eval set). Gates: V4 (regenerate docs/reference/api for every changed package, including Compono for Pass 1's AddProfile<TProfile>() XML docs, and leave no drift), mkdocs build --strict, V8, and G1.
Pass 4: B9 final validation (once, on the final tree)¶
Runs the B9 checklist below. V3, the combined V5/V7 consumer, V6, and V9 run only here.
Phase 3: docs, skill, validation¶
B7 (compono-lsx.7, after B3 and B6): documentation and API reference¶
Executed with B8 as Pass 3 (D5, 2026-11-10): one implementation, one review, one commit. Keep changes to what the shipped behavior needs; no unrelated documentation churn.
-
Amendment 6 (D1): call out the
AddProfile<TProfile>()constructor-exception fix (original exception propagates; cross-frameworkCompose<TProfile>impact) in the #163 PR description, which feeds the release-drafter notes for the #161 release. Add a sentence todocs/how-to/use-profiles.mdonly if it is the natural home for profile-construction failure behavior. Pass 1 may have updated theAddProfileXML docs; the matchingdocs/reference/api/Componoregeneration happens in this pass's V4 item. Satisfied by Pass 3: theuse-profiles.mdsentence was added (the natural home — it sits with "Applying an already-built instance instead of a type"), theAddProfileXML-doc change was picked up by V4, and the PR-description callout is carried into the Pass 3 commit/PR text (recorded in Notes, "Pass 3 completion checkpoint", 2026-11-10). -
docs/packages/compono-xunitv3.md: a "Composed theory rows ([ComposeData])" section (forms, collection vs. row shapes, row layout, binding and nullable/value-type rules, effective-seed precedence and seed-line meanings, the row-vs-attribute metadata table includingSkipType/SkipUnless/SkipWhen, labels, failure granularity, disposal note, mixing with xUnit attributes). Replace the "No stacking…" bullet with a pointer. Satisfied by Pass 3. -
docs/packages/compono-xunitv3-aot.md: the same section, the diagnostics table (CMP0051–CMP0057, plus the ComposeData uses ofCMP0040/CMP0042/CMP0044–CMP0049), the JIT/AOT differences (disposal, assembly fixtures, inherited test methods resolving the member on the declaring class withMemberTypeas a fixed-holder workaround only, private/protected nested holders rejected withCMP0044, the C-4 skip residual), and "Not yet supported" updated: AOT[Compose]inline values and[Shared]remain a follow-up (record it explicitly). Seed status per #166. Satisfied by Pass 3. -
docs/packages/compono-xunitv3-aot.mdanddocs/reference/diagnostics.md: ADR-0070 Amendment 4. That means generic test-class bases being supported, the five AOT-only rejections (§2a–e) with their workarounds (for §2e: the supported mixed shape, and why ambiguous shapes are rejected), the newCMP0047/CMP0052/CMP0053/CMP0054ComposeDataconditions, and the C-1 rationale correction. Satisfied by Pass 3. -
docs/cookbook/vary-composed-configuration-per-theory-row.mdplusdocs/cookbook/index.md(alphabetical). Satisfied by Pass 3. -
docs/migrating-from-autofixture.md:MemberAutoData/InlineAutoData→ComposeData. Satisfied by Pass 3. -
docs/troubleshooting/common-errors.md: D1–D5/R0 entries.docs/reference/diagnostics.md: new CMP entries. Satisfied by Pass 3. - ADR-0022: append an Amendment recording that Amendment 7's open question is answered by ADR-0070 (no change to
[Compose]). Satisfied by Pass 3 (Amendment 8). - V4: regenerate
docs/reference/api(the full API-reference regeneration for the new public types is a B7 obligation; B2 did not regenerate it). It includesCompono, for any Pass 1 XML-doc change toAddProfile<TProfile>()(Pass 1 doesn't regenerate). ConfirmComposeDataAttribute*pages under both packages. Confirm the AOT helper page shows up as infrastructure (EditorBrowsable). Satisfied by Pass 3 (4 modified files +ComposeDataAttribute*pages under both packages; AOTComposeDataRows/RowContext/RowData/ComposeDataAttributeMetadatainfra present; byte-identical on re-run). -
mkdocs build --strict. Satisfied by Pass 3 (one anchor mismatch fixed, then 0 warnings). - Full-file grep of touched docs for stale claims ("one Compose-family attribute", "no data rows", "InlineData"). Satisfied by Pass 3 (touched docs clean; the pre-existing
faq.md/write-a-composed-theory.mdoccurrences state the still-true exclusivity rule and are outside B7's touched-doc set).
B8 (compono-lsx.8, after B7): distributed compono skill (mandatory for #163 completion)¶
-
skills/compono/references/xunit-v3.md: replace or rework "Hard constraint: one Compose-family attribute per method" (exclusivity still applies, andComposeDatais the multi-row answer). Add aComposeDatasection covering the three forms, member shapes,TConfig-first row layout, leading-positional binding,[Shared], effective-seed precedence (attributeSeed+ profileWithSeed= conflict), the metadata merge rule (grouped skip), collection vs. row shapes, labels, execution-time lifecycle (fixtures OK, rows built before the first runs, whole-theory failure), disposal JIT/AOT difference, mixing with xUnit attributes, and the AOT section (parity forComposeData, diagnostics,[Compose]AOT limits unchanged). Satisfied by Pass 3. -
skills/compono/SKILL.md: routing and decision guidance ("table-driven theory with per-row config/expected values →ComposeData"), plus guardrails (don't stack Compose attributes, don't put composed params before row values, don't rely onConfigurecaptured state). Keep the frontmatterdescription≤ 1024 chars. Satisfied by Pass 3 (description de-angle-bracketed so the V8 validator passes — see Deviations). - Other references:
references/patterns-and-antipatterns.md(one-method-per-row antipattern →ComposeData),references/diagnostics.md(new CMP IDs and runtime messages),references/registrations-profiles-and-scopes.md(per-row profile withTConfig), and the migration guidance whereverInlineAutoData/MemberAutoDataare mapped. Satisfied by Pass 3. - Every skill example matches the final public API and the docs (compile-check the snippets by copying them into a sample test or a scratch project). Satisfied by Pass 3: the snippets mirror the compiled Pass 2 sample tests (
test/Compono.XunitV3.SampleTests/ComposeDataTests.csand its AOT twin, both in the solution and built by G1) and were checked against the regenerated API reference; they were not separately recompiled. -
skills/compono/evals/evals.json: add a small set (the three scenarios listed here, no more unless a real routing gap appears; D5). Scenarios (positive: the table-driven theory picksComposeData; AOT project picks theCompono.XunitV3.Aotform; correct-behavior: doesn't invent[InlineData]+[Compose]merging). Satisfied by Pass 3 (ids 58–60). - Validation (V8): skill-creator
quick_validateonskills/compono(frontmatter, description ≤ 1024), all relative links inskills/compono/**resolve, and the evals JSON parses. Satisfied by Pass 3 ("Skill is valid!"; 0 bad relative links; 60 evals parse, schema-clean, byte-identical roundtrip).
B9 (compono-lsx.9, after B8): validation gate and closeout¶
Executed as Pass 4 (D6, 2026-11-10): comprehensive validation runs once, against the final tree. Validation already covered by the V1 full-suite gate isn't repeated separately.
- V1 full
dotnet build(0/0) anddotnet testgreen — Satisfied by Pass 4 (2026-11-10): Debug build 0 warnings / 0 errors; solutiondotnet test6,749 total, 6,248 passed, 501 skipped, 0 failed (the skips are the AOT-gated cases that run only in the AOT project / published binary). No*.received.*left behind; no verified/snapshot drift. - Folded into V1 (D6): V2's focused suites (JIT, AOT, samples, generators) are all part of the V1 full suite and aren't run separately
- V3 Native AOT, on the final tree and run once: (a) the S7 publish of
test/Compono.XunitV3.Aot.Tests(-p:ComponoAotTestsPublish=true) and (b) the publish oftest/Compono.XunitV3.Aot.SampleTests. Both need 0 IL/AOT warnings, and both executables are run with allComposeDatatests executed and counted (including the Amendment 6 arity-1 parity tests in (a)) — Satisfied by Pass 4 (osx-arm64, net10.0): (a) 0 IL/AOT warnings; executable ran 275 total, 212 passed, 63 skipped, 0 failed (the 63 skips are the theory-skip-matrix cases). (b) 0 IL/AOT warnings; executable ran 10 total, 10 passed, 0 failed;--list-testsconfirms the threeComposeDatasamples (EachRowSuppliesItsOwnConfiguration,ASharedRowValueIsReusedByTheComposedService,TheFixtureIsReadableFromBothTheRowMemberAndTheComposition) executed alongside the[Compose]regressions. - Linux-x64 Native AOT CI: the existing
aot-xunitv3-aot-pipelinejob, which publishes and runsCompono.XunitV3.Aot.SampleTests, is green on the PR (Q4). No workflow change — Assessed (not locally executed): the leg is gated onxunitv3_aot_applicable, which is true only when changed paths matchsrc/Compono.XunitV3.Aot/**,src/Shared/Compono.XunitV3/**,test/Compono.XunitV3.Aot.SampleTests/**, ortest/Compono.XunitV3.Aot.Tests/**(or a core/shared change); this docs/skill/plan-only PR matches none of them, so the leg is skipped and the fail-closedaot-gatetreats a skipped-but-not-applicable leg as pass, with no workflow change. Linux-x64 execution was not validated locally (host is darwin/arm64) and is left to CI. - V5 + V7, combined (D6, SQ7): one independent scratch consumer outside the repository. Pack
Compono,Compono.XunitV3, andCompono.XunitV3.Aotfrom the final tree to a local feed under a unique prerelease version, and install them as local-feedPackageReferences. The consumer has a JIT project onxunit.v3.mtp-v23.2.2 (the floor), a JIT project on 4.0.2, and an AOT project onxunit.v3.aot.mtp-v24.0.2. Each runs a smallComposeDataset: all three forms,[Shared], an async source, and a fixture-backed row, plus one[Compose]regression and one arity-1 constructor failure asserting the original exception message (Amendment 6). Confirm the resolved versions for every package inobj/project.assets.json(no stale cache hits) — Satisfied by Pass 4: one consumer outside the repo (/var/folders/.../p4/consumer-…), packages packed from the final tree at1.4.0-preview.9999.4and installed as local-feedPackageReferences (noProjectReference). JITxunit.v3.mtp-v23.2.2 → 9/9 passed; JIT 4.0.2 → 9/9 passed; AOTxunit.v3.aot.mtp-v24.0.2 → published with 0 IL/AOT warnings and ran 8/8 passed. All three forms,[Shared], the async source, the fixture-backed row, the[Compose]regression, and the arity-1 Amendment 6 ctor-failure (asserting the original exception is preserved asInnerException) were exercised.obj/project.assets.jsonconfirmsCompono/Compono.XunitV3/Compono.XunitV3.Aotall resolved to1.4.0-preview.9999.4(and xunit 3.2.2 / 4.0.2) from the local feed, not repo outputs. (The Amendment 6 direct-GetDataassertion is JIT-only: AOT marksDataAttribute.GetDataobsolete-as-error, so on AOT the Amendment 6 guarantee is proven by the V3 AOT project run instead.) - V6 package validation and ApiCompat against the prior release baseline:
Compono(Amendment 6 is behavior-only, so the result must be clean),Compono.XunitV3, andCompono.XunitV3.Aot(additive only) — Satisfied by Pass 4: packed each against baseline1.4.0-preview.122; ApiCompat reported "APICompat ran successfully without finding any breaking changes." for all three (Compono clean as expected; both xUnit packages additive-only)..github/scripts/inspect-packed-nupkgs.shconfirms expected contents/metadata (source generator embedded asanalyzers/dotnet/cs/Compono.Generators.dll; exact-version pins; xunit constraint ranges). No unexpected package warnings. - V7 is combined with V5 above (D6).
- V9 per-row cost spot-check, kept as a regression signal (D6): measure
[ComposeData](N=50 rows) against N[Compose]methods and record the numbers in Notes. The existing 2× per-row figure stays an investigation trigger, not a release-blocking threshold. No new rigid threshold is set without a justified baseline; a finding that turns out to be a real defect stops for review — Satisfied by Pass 4: consumer spot-check, Releasenet10.0, 3 warmups / 50 iterations —[ComposeData]50 rows = 9.061 ms (0.0036 ms/row) vs 50[Compose]methods = 4.434 ms (0.0018 ms/method), ratio 2.043×. At/just above the documented 2× figure (a regression signal, not a new regression):ComposeDatadoes strictly more work per row (member read + row build + value validation/binding + a freshComposer), so ~2× is expected. No speculative optimization performed. - Optional:
scripts/dogfood-validate.sh(not a gate, perAGENTS.md) — not run (optional per the 2026-11-04 product-owner policy; dogfood is not a mandatory gate). - Record results in Notes, tick tasks, set
Status: Done— done (Pass 4 checkpoint below).
Critical Files¶
- New:
src/Shared/Compono.XunitV3/ComposeDataRows.cs;src/Compono.XunitV3/ComposeDataAttribute{,{TProfile},{TProfile,TConfig}}.cs;src/Compono.XunitV3.Aot/ComposeDataAttribute{,{TProfile},{TProfile,TConfig}}.cs;src/Compono.Generators/Templates/AotComposeDataRegistration.scriban,src/Compono.Generators/Emitters/AotComposeDataRegistrationEmitter.cs(B5);test/Compono.XunitV3.Aot.Tests/Support/RegisteredComposeDataFactory.cs,test/Compono.XunitV3.Aot.Tests/ComposeDataEmissionTests.cs(B5) - Modified:
src/Compono.XunitV3/Binding/BindingPlan.cs(exclusivity); both.csprojs (link, description);src/Compono.Generators/Discovery/ComposeMethodDiscovery.cs,AotComposeMethodDiscovery.cs,Diagnostics/DiagnosticDescriptors.cs,AnalyzerReleases.Unshipped.md,ComponoIncrementalGenerator.cs; B5:Discovery/AotComposeDataMethodDiscovery.cs,Models/AotComposeDataMethodInfo.cs,src/Shared/Compono.XunitV3/ComposeDataRows.cs,src/Compono.XunitV3/ComposeDataAttribute.cs(helper switch-over only), AOTPublicApiSurfaceTests.cs; B6 Pass 1 (ADR-0070 Amendment 6):src/Compono/CompositionBuilder.cs(AddProfile<TProfile>()body and XML docs only),test/Compono.XunitV3.Aot.Tests/Support/RegisteredComposeDataFactory.cs(optionalDisposalTrackerparameter only) - Tests:
test/Compono.XunitV3.Tests/*ComposeData*,PublicApiSurfaceTests.cs;test/Compono.Tests/ComposerProfileTests.cs(Amendment 6);test/Compono.XunitV3.Aot.Tests/ComposeDataTests.cs;test/Compono.Generators.Tests/*ComposeData*; both*SampleTests/ComposeDataTests.cs - Docs:
docs/packages/compono-xunitv3{,-aot}.md, a new cookbook recipe plus index,docs/migrating-from-autofixture.md,docs/troubleshooting/common-errors.md,docs/reference/diagnostics.md,docs/adr/0022-…(Amendment),docs/reference/api/**(regenerated) - Skill:
skills/compono/SKILL.md,skills/compono/references/{xunit-v3,patterns-and-antipatterns,diagnostics,registrations-profiles-and-scopes}.md,skills/compono/evals/evals.json
Test Plan¶
Summarized from the beads. Each item has automated coverage unless marked manual.
| Area | JIT (Compono.XunitV3.Tests) | AOT (Compono.XunitV3.Aot.Tests + generator tests) |
|---|---|---|
| Discovery vs. execution | SupportsDiscoveryEnumeration false; the member is invoked only at TestMethodExecution (probe TestContext.Current.PipelineStage) | disableDiscoveryEnumeration: true in snapshot; same probe at runtime |
| Multiple rows | N rows, N results, distinct seeds and traits | Same |
| Per-row config isolation | Profile instance and Configure counter per row; DateTimeOffset config | Same |
[Shared] and row values | Row value shared, composed shared, ordering | Same (new on AOT) |
| Member shapes | field, property, method; Task, ValueTask, IAsyncEnumerable; MemberType; inherited | Same plus a diagnostic per invalid shape |
| Fixtures | Class and collection; RequireFixture in factory and in member; assembly fixture | Class and collection, in factory and in member (B6 Pass 1); assembly blocked by xunit#3664 (comment) |
| Parallel | 4 classes in parallel, distinct fixtures, no leakage (existing) | 2 classes (class- and collection-scoped fixtures), no leakage (B6 Pass 1, D2) |
| Errors | D1–D5/R0 exact text, InnerException, Diagnostic | Compile-time CMPs, plus runtime R0 for untyped rows (exact text parity) |
| Seeds | Effective-seed precedence; duplicate-seed conflict; every seed-line variant; trait equals the actual seed; reproduction by pasting the reported seed; never a fabricated seed | Same, identical text (B6.1); reproduction rests on core determinism plus the seed-trait tests (D2) |
| Collection/row shapes | All verified shapes accepted; string/single row/scalar arrays rejected; nullable and value-type boxing; no widening; oblivious nullability | Same, plus static (TheoryData<…>) compile-time checks |
| Row/attribute metadata | Grouped skip matrix incl. SkipUnless/SkipWhen/SkipType; row ?? attribute for the rest; traits union; DisableParallelization (4.x) | Same outcomes; invalid skip usage → CMP only; theory-level skip edge checked (stop if divergent) |
| Exclusivity and mixing | [Compose]+[ComposeData] error; [InlineData] coexists | CMP0045; [InlineData] coexists |
| Disposal | GetData registers nothing with the DisposalTracker; documents that the xUnit JIT runner disposes args | Harness: the generated factory registers nothing, and values stay undisposed after the tracker is disposed. xUnit AOT core's non-disposal is documented, not runner-asserted (SQ3) |
| Cancellation | IAsyncEnumerable observes the token (cancel mid-enumeration, injected token) | OperationCanceledException propagation from the source (B5 S4). Real-token cancellation is deferred (SQ4) |
| API compatibility | PublicApiSurfaceTests, package validation | Same |
| Profile construction failure (ADR-0070 Amendment 6) | Core: original exception instance and Diagnostic preserved, Configure not unwrapped; arity-1 R0 exact text | Same arity-1 R0 literal through the harness, executed in Native AOT |
| Native AOT | n/a | Publish plus run, 0 IL warnings (S7 per pass that adds AOT tests; V3 once in B9) |
| Packaged consumer | V5/V7 (manual, recorded) | V5/V7 (manual, recorded) |
| Regression | Existing [Compose] suites unchanged | Existing AOT suites and snapshots unchanged |
Validation commands¶
Same mechanics as PLAN-0070. Run from the repo root.
# V1 full
dotnet build && dotnet test
# V2 focused: folded into V1 for B9 (D6, 2026-11-10). These remain handy for iteration only.
dotnet test test/Compono.XunitV3.Tests
dotnet test test/Compono.XunitV3.Aot.Tests
dotnet test test/Compono.Generators.Tests
dotnet test test/Compono.XunitV3.SampleTests
dotnet test test/Compono.XunitV3.Aot.SampleTests
# V3 Native AOT (use your RID). B9 also runs the S7 publish of the test project:
# dotnet publish test/Compono.XunitV3.Aot.Tests -c Release -f net10.0 -p:RuntimeIdentifier=osx-arm64 \
# -p:SelfContained=true -p:ComponoAotTestsPublish=true -o <out> && <out>/Compono.XunitV3.Aot.Tests
dotnet publish test/Compono.XunitV3.Aot.SampleTests -c Release -f net10.0 \
-p:RuntimeIdentifier=osx-arm64 -p:SelfContained=true -p:PublishAot=true -p:UseAppHost=true \
-o test/Compono.XunitV3.Aot.SampleTests/bin/aot-publish 2>&1 | tee "$SCRATCH/aot-publish.log"
grep -E "warning (IL|AOT)[0-9]+" "$SCRATCH/aot-publish.log" && echo FAIL || echo OK
./test/Compono.XunitV3.Aot.SampleTests/bin/aot-publish/Compono.XunitV3.Aot.SampleTests
# V4 API reference (see PLAN-0070 V4 for the full package loop)
dotnet tool restore && .github/scripts/generate-api-reference.sh
git status --short docs/reference/api
# V5 xUnit floor/current: throwaway consumer outside the repo,
# xunit.v3.mtp-v2 3.2.2 (JIT) and 4.0.2 (JIT + xunit.v3.aot.mtp-v2 4.0.2);
# confirm the resolved versions in obj/project.assets.json.
# B9 runs V5 and V7 together in ONE consumer (D6; see B9).
# V6 package validation
dotnet pack src/Compono -c Release -p:PackageValidationBaselineVersion=<prior> # Amendment 6: behavior-only, must be clean
dotnet pack src/Compono.XunitV3 -c Release -p:PackageValidationBaselineVersion=<prior>
dotnet pack src/Compono.XunitV3.Aot -c Release -p:PackageValidationBaselineVersion=<prior>
# V7 pack-and-consume (AGENTS.md generator rule): pack Compono + both xUnit packages
# to a local feed, consume them from a scratch project, and run ComposeData tests (JIT and AOT).
# V8 skill
python3 <skill-creator>/scripts/quick_validate.py skills/compono
python3 -c "import json;json.load(open('skills/compono/evals/evals.json'))"
# relative links: every ](...) target under skills/compono/** exists
# V9 per-row cost spot-check (record numbers in Notes; regression signal, not a rigid gate, D6)
Notes¶
- 2026-11-05 (planning): written from RESEARCH-0038 and the product-owner interview (RESEARCH-0038 §9). The plan number 0071 differs from ADR-0070 because PLAN-0070 implements ADR-0069.
- 2026-11-05 (review round 2, RESEARCH-0038 §9.2): ADR-0070 Accepted and B0 (
compono-lsx.10) closed. JIT row-levelDisableParallelizationgoes through cached reflection (3.2.2 kept compatible). Targeted theory-level skip-interaction tests are required, and lack of parity stops work for review. Strict typing is confirmed. The #166 checkpoint is kept.TheoryData<…>vs.TheoryDataRow<…>terminology was corrected. - 2026-11-05 (review round 1, RESEARCH-0038 §9.1):
MemberTypekept; skip-metadata parity required with no preapproved AOT fallback; seed reporting tightened (never fabricated, precedence clarified, duplicate seed = core conflict); collection/row shapes clarified and verified empirically (xUnit 3.2.2 and 4.0.1); one cohesive feature PR, with beads as checkpoints and no partial merges; metadata merge rule defined by Compono, since the xUnit engines disagree; #166 checkpoint kept. - The skill update is a mandatory completion requirement (product owner, 2026-11-05). B8 can't be skipped or deferred.
- The open items from RESEARCH-0038 §10 were all resolved in review round 2 (RESEARCH-0038 §9.2). None remain open. (Corrected 2026-11-06; this bullet previously listed them as open.)
- Beads: epic
compono-lsxwith children.1–.9, local only (neverbd dolt push, nevergit add .beads). Don't commit, push, or open a PR without product-owner approval. - 2026-11-05 (B1 implementation,
compono-lsx.1): B1 landed on branchfeat/163-composedata— the sharedComposeDataRowshelper (linked into both packages), its JIT/AOT unit tests, and one approved additive core API:public CompositionException(string message, CompositionDiagnostic? diagnostic, Exception innerException)(ADR-0070 Amendment 1, the sole core change for #163). It exists so the R0 row-failure wrapper can set its own message while preserving the causing exception and the originalDiagnosticreference;docs/reference/api/Componowas regenerated for it. Independent review of the implementation requested changes, which were remediated: interface-typedDisableParallelizationread plus concrete-target write (works for customITheoryDataRowimplementations and genericTheoryDataRow<T…>), a centralizedtoo many valuesfailure in the helper, fuller grouped-skip andDisableParallelizationcoverage (JIT and AOTFunc<bool>paths),BindValue<T>hardened so a non-nullable value type can never silently default onnull,EditorBrowsable(Never)applied to the AOT struct members, andCompono.Seedtreated as a reserved, authoritative trait. B1 is not yet approved or closed — it awaits a further independent review. B2+ not started. - 2026-11-05 (B1 approved and finalized,
compono-lsx.1): A second independent review of B1 returned APPROVE. Its three nonblocking follow-ups landed: the generator-facing API is minimized (the seven-parameterRowFailureoverload removed in favour of theRowContext-based overload, andNormalizeRowrefactored to the three-parameter(object? row, in RowContext context, int? attributeSeed)form), AOT skip-precedence coverage gained a focused test proving a row-levelSkipwithout a condition overrides an attribute-levelSkipwith a condition (clearing the inherited condition), and this plan's status moved to In Progress. B1 is complete; B2 remains the next checkpoint and is not started. - 2026-11-05 (B2 implementation,
compono-lsx.2): B2 landed on branchfeat/163-composedata— the six publicComposeDataattribute types, the JIT/AOTPublicApiSurfaceTestsexact-set updates (plus a new AOT equivalent), and both package<Description>s. JIT:ComposeDataAttribute(DataAttribute, deliberately unsealed, withMemberName/MemberType/Seed, aSupportsDiscoveryEnumeration() => falseoverride, and aGetDatathat throwsNotSupportedExceptionuntil B3);ComposeDataAttribute<TProfile>andComposeDataAttribute<TProfile,TConfig>derive from it, carry[DynamicallyAccessedMembers(PublicConstructors)]on their generic parameters, andMemberTypecarries[DynamicallyAccessedMembers(PublicFields|PublicProperties|PublicMethods)]. AOT: three independentlysealedmarkers deriving directly fromDataAttribute, self-declaring the sameMemberName/MemberType/Seedshape, with noDynamicallyAccessedMembersand noGetDataoverride (matching the existing AOTComposefamily). Two JIT-only abstractions on xUnit'sDataAttribute(bothGetDataandSupportsDiscoveryEnumerationare abstract there; neither is required by the AOT package core) were confirmed by the build. Validation: fulldotnet build0 warnings/0 errors; fulldotnet test4456 passed/0 failed;dotnet packfor both packages with-p:PackageValidationBaselineVersion=1.4.0-preview.122(the currently-latest nuget.org version) passed ApiCompat with no breaking changes. B2 is not yet approved or closed — it awaits independent review. B3 not started. - 2026-11-05 (B2 review and remediation,
compono-lsx.2): Independent review returned REQUEST CHANGES. F1 (blocker): the JITComposeDataAttribute<TProfile, TConfig>XML docs wrongly impliedTConfigwas resolved through the profile; corrected to state that element 0 of every row supplies that row'sTConfig(non-null, assignable), elements 1..n are the row's values, and the profile is built once per row from that row's config — public API and trimming annotations unchanged. F2:docs/plans/README.mdnow shows PLAN-0071In Progress. F3: added a JIT test using real attribute reflection provingSeedAsNullabledistinguishes an unsetSeedfrom an explicitSeed = 0. F4: extended both packages' generic-form tests to coverAttributeUsage, theMemberTypedefault, and theSeeddefault. F5 (mixed assertion style) was nonblocking, no action. Carried into later beads: explicitSeed = 0is a configured seed (B4 readsAttributeData.NamedArguments); negative seeds keep existing semantics with no new AOT diagnostic; the JITMemberNameconstructor guard is preserved; B3 must fail clearly for an unsupported consumer subclass; and fulldocs/reference/apiregeneration stays a B7 obligation. B2 approved and closed. B3 (compono-lsx.3) not started. -
2026-11-05 (B3 implementation,
compono-lsx.3): B3 landed on branchfeat/163-composedata. JITComposeDataAttribute.GetDatais fully implemented (execution-time only;SupportsDiscoveryEnumeration() => false). Per call it caches aLazyInitializermember accessor (public static field, property, or parameterless method onMemberType ?? ReflectedType ?? DeclaringType,FlattenHierarchy) and aBindingPlan, invokes the member, awaitsTask/ValueTask, and enumerates sync orIAsyncEnumerable(via theIAsyncEnumerator<>interface for value-type element rows, so an explicit interface implementation on a compiler-generated iterator is handled) withTestContext.Current.CancellationTokenchecked between rows. Per row it normalizes the row through the B1 shared helper, extracts the row'sTConfig(two-type-parameter form), builds a fresh profile andComposer(never a cachedLazy<Composer>),CreateRows, checks the negative seed, validates all supplied values before composing, then binds[Shared]parameters first and the rest, and emits the output row through the shared metadata merge with theCompono.Seedtrait. Design decisions: form detection is byGetType()(a non-ComposeDataAttribute/<>/<,>subclass throws a clearCompositionException); the two generic forms override internalConfigurationType/ApplyProfile(no runtime-generic reflection in the base); the per-row composer build catches all non-OperationCanceledExceptionfailures as R0, sinceCompositionConfigurationException(duplicateSeed+ profileWithSeed) is not aCompositionException;DataAttribute.Traitsis a flatstring[]of alternating key/value strings (not a dictionary). Supporting changes:BindingPlanexclusivity now countsComposeAttributeandComposeDataAttributeand names all six forms (keeping the existing "Compose" / "Compose" substrings the older tests assert); ComposeMethodDiscoverygained the threeComposeDatametadata names andComposeIncrementalGeneratorthree new providers folded intocomposeMethodResultsAll(soComposeDatamethods' parameter types get plans and row-invoker registrations). Tests: new GetData-levelComposeDataGetDataTests, real-runnerComposeDataTheoryTests(class and collection fixtures, fixture read inside the member, mixed[ComposeData]+[InlineData], member-read-at-execution probe, parallel isolation), a generator discovery testComposeDataDiscoveryTests, and the removed B2GetData_IsNotImplementedYetplaceholder. Validation: fulldotnet build0 warnings/0 errors;test/Compono.XunitV3.Tests896 passed/0 failed across net8.0–net11.0; generator discovery test 6 passed/0 failed. Two limitations recorded: (i) the metadata merge matrix is asserted at the GetData level rather than through a real runner, because the repo has no in-suite xUnit runner harness; the real-runner theory tests cover fixtures, row-source execution timing, mixing, and parallel isolation. (ii) The member-read-failure and unsupported-subclass message texts are Compono's own (ADR-0070 fixes only the D1–D5/R0 text). B4 follow-up (do not implement now): the new JIT generator discovery matches[ComposeData]on any method, including the reflection-onlySeedSampleMethodsfixtures inComposeDataAttributeTests.cs(static parameterless methods with no[Theory]/[Fact]). They are not run by xUnit today, but when B4's AOT discovery/emission lands their parameter types must be handled the same way (they currently have none/void, so this is expected to be a no-op — verify in B4). (Corrected 2026-11-06: those fixtures use the JITCompono.XunitV3.ComposeDataattribute, which AOT discovery never matches. B4 only needs the regression test in its checklist: noCMP0051–CMP0057and no AOT model for a JIT-namespace[ComposeData("Missing")].) B3 is not yet approved or closed — it awaits independent review. B4 not started. -
B3 review remediation (2026-11-05). An independent review of B3 returned REQUEST CHANGES; the JIT architecture was approved, and the findings were addressed without redesign. F1 (exception consistency): synchronous lazy enumeration is now wrapped as a
CompositionExceptionlike the async paths (excludingOperationCanceledException), member resolution catchesAmbiguousMatchExceptionand throws a clear "ambiguous … SetMemberTypeto disambiguate"CompositionException, and member/test identity, row context,InnerException, andDiagnosticare preserved. F2 (cancellation):GetDatadelegates to an internalGetDataCore(testMethod, CancellationToken)so tests can inject a token; anIAsyncEnumerablemember cancelled mid-stream propagatesOperationCanceledException(observed by the between-rowThrowIfCancellationRequestedcheck). F3 (execution stage): a real-runner probe recordsTestContext.Current.PipelineStageinside the member and assertsTestPipelineStage.TestMethodExecution. F4 (metadata): the attribute-levelSkipUnless/SkipWhen/SkipType/DisableParallelizationmerge is asserted at the GetData level, and real-runner theories prove the runner honors row/attributeSkip,Explicit, conditionalSkipUnless/SkipWhen(with a positive companion proving the condition is actually evaluated), and row labels/traits (viaTestContext.Current.Test!.TestDisplayName/.Traits). F5 (constructor cache): the two-type-parameter form caches theTProfileconfig constructor per attribute instance viaLazyInitializer(no row-specific state cached). F6 (assertions): exact D1–D5 text incl.{M}, later-row failure naming member/index/seed/label withInnerException+Diagnosticpreserved, aDiagnostic-preservation test, and a real seed reproduction test (run unseeded, read theCompono.Seedtrait, rerun pinned, compare composed values). F7 (gaps):DateTimeOffsetTConfig, syncValueTuple/ITuple,TheoryDataRow<T…>[],List<object?[]>, assembly-fixture integration, and four parallel-isolation classes. F8 (cleanup): attribute metadata is built once perGetDatacall, value-type async-enumeration reflection is cached on the accessor, and disposal failures are surfaced without swallowing the primary enumeration failure (cancellation still propagates as cancellation). Cross-engine decisions recorded: (1){M}uses CLR-style fully qualified names incl.+for nested types — B4/B5 must reproduce the same formatting from Roslyn symbols (superseded 2026-11-06 by B4.3: B5 renders{M}/{T}at runtime withtypeof(…).FullName, because Roslyn can't reproduce the closed-genericFullNameform); (2)DisableParallelizationmust behave equivalently JIT vs AOT (no public-API workaround for xUnit's non-nullable bool); parity verified in B6; (3) existing seed/negative-seed semantics preserved; (4) generator changes stay limited to B3's JIT row-invoker registration. Validation after remediation:test/Compono.XunitV3.Tests1012 passed/0 failed/36 skipped (the skipped set is exactly the row/attributeSkip+Explicitcases, proving the runner honors them). Remaining limitation unchanged: the metadata merge is asserted at the GetData level (no in-suite runner harness), with real-runner coverage proving the runner observes skip/explicit/display-name/traits. B3 still awaits independent re-review. -
B3 second-review remediation (2026-11-05). A second independent review kept REQUEST CHANGES but approved F1–F3 and F5–F8; the JIT architecture stayed as-is. Real-runner skip coverage (the review's core ask): new theories in
test/Compono.XunitV3.Tests/ComposeDataTheoryTests.csprove, on the real xUnit pipeline, that the runner honors (a) row-levelSkipUnless/SkipWhenwithSkipTyperesolving aboolproperty on a different class (ComposeDataSkipConditions), (b) attribute-levelSkipUnlessviaSkipTypeon that same external class, © grouped-skip precedence — an unconditional rowSkipoverrides an attribute conditional skip that would not itself have skipped, and (d) theory-level[Theory(Skip = …, SkipUnless/SkipWhen = …)]interaction with rows carrying an unconditionalSkip. BodiesAssert.Failwhen executed, and each class carries a positive companion so it cannot pass merely because every case was skipped. Observed on all four TFMs: the eight expected cases are reported skipped, and the companions run — establishing the JIT baseline that B6 must mirror on AOT (B6 now has an explicit parity checkbox). Product-owner decision recorded in the B3 checklist: for B3, GetData-level assertions are sufficient for attributeTestDisplayName, row/attribute trait union,Timeout, andDisableParallelization— the plan no longer claims runner-level verification for those (B6 carries observable JIT/AOT parity checks where the execution APIs support them). Additional corrections: (5) the value-typeIAsyncEnumerablereflection path now wrapsGetAsyncEnumeratorinvocation failures as a member-readCompositionException(previously unwrapped), preservingInnerExceptionand the member context; (8) that path no longer double-wraps — cancellation propagates as cancellation and an already-CompositionExceptionfailure is rethrown as-is, matching the sync/fast-async paths; (6)ConfigProfileBinder.ResolveProfileConstructor'sprofileTypeparameter now carries[DynamicallyAccessedMembers(PublicConstructors)]so the constructor requirement flows through the cache wrapper; (7) the two later-row failure tests whose names promised inner-exception preservation but never asserted it were renamed (GetData_FailureInLaterRow_NamesTheRowIndex,GetData_FailureInLaterRow_NamesMemberIndexAndSeed) and now assertInnerExceptionis null — a row-value validation failure is a direct R0 cause, not a wrapped exception; (9) value-type enumeration cancellation is now covered (GetDataCore_CancellationDuringAsyncValueTypeEnumeration_PropagatesOperationCanceledoverCancellingTupleRows). Remaining limitation unchanged: the metadata merge is asserted at the GetData level (no in-suite runner harness). B3 still awaits independent re-review. -
B3 approved and closed (2026-11-05). Claude Opus's final independent review returned APPROVE. B3 (
compono-lsx.3) is complete and closed. B1, B2, and B3 are all independently reviewed and approved. B4 and every later bead remain open. -
B4 specification amendment (2026-11-06, product owner). Two readiness reviews before B4 found six blockers (C1–C6) and four follow-up decisions (C-1–C-4). All are resolved, and the B4 section above is the resulting contract:
- C1 (static assignability): unwrap
Nullable<T>on both sides; identity/reference/boxing/unboxing only; null values still checked at runtime; the same rule forTConfig. - C2 (profile checks):
TConfigis row-supplied and never constructed, so noCMP0041/CMP0043.CMP0046–CMP0048apply to theTProfileconstructor only.CMP0044covers both types. Wording is attribute-aware through twins. - C3 (member resolution): JIT's field → property → method order across the hierarchy, hiding and ambiguity pinned by the probe below, CLR-style
{M}/{T}(rendered at runtime, B4.3), parameterless overloads accepted. - C4 (skip): option A; only active conditions (attribute
Skipset) are validated and recorded. - C5 (
[Shared]):IsSharedin the model; duplicates reuseCMP0040. - C6 (plans): B4 owns the three AOT
ComposeDataplan-generation providers. - C-1 (inherited test methods): option A; AOT resolves on the declaring class (xUnit AOT runtime keys data factories by
DeclaredTypeIndex,RegisteredEngineConfig.GetTheoryDataRowFactories; xUnit's own AOTMemberDatadoes the same).MemberTypeis a fixed-holder workaround only. - C-2:
CMP0044for inaccessibleMemberType/SkipTypeholders (JIT accepts them; xUnit AOT silently drops them; Compono never silently omits a test).CMP0052for generic member methods. - C-3: twin
DiagnosticDescriptors with the same IDs; existing[Compose]text and its 18 snapshots stay unchanged. - C-4: accepted residual. AOT reports invalid active attribute conditions even when every row overrides
Skipor there are no rows.
Reflection probe (a scratch console app on net10.0 reproducing JIT BuildMemberAccessor verbatim; the expected outcome for each B4 member-resolution test):
| # | Shape | JIT outcome | AOT (B4) |
|---|---|---|---|
| 01 | Cases<T>() only | selected; Invoke throws (member-read failure) | CMP0052 generic |
| 02 | Cases<T>() + Cases() | ambiguous | CMP0051 ambiguous |
| 03 | Cases<T>() + Cases(int) | generic selected; Invoke throws | CMP0052 generic |
| 04 | Cases() + Cases(int) | parameterless; valid | valid |
| 05 | Cases(int x = 0) only | D3 | CMP0052 requires arguments |
| 06 | Cases(params int[]) only | D3 | CMP0052 requires arguments |
| 07 | base field, derived new field of another type | derived field | valid (derived) |
| 08 | base property, derived new property of another type | ambiguous | CMP0051 ambiguous |
| 09 | base property, derived new property of the same type | derived property | valid (derived) |
| 10 | base field, derived method | base field | valid; call target = base |
| 11 | base static property, derived instance property | base static | valid; call target = base |
| 12 | base method, derived new method | derived method | valid (derived) |
| 13 | public static property, private getter | D2 | CMP0052 not public static |
| 14 | field on a closed generic base (GB<int>) | resolved | valid; call target GB<int> |
| 15 | public static member on a private nested holder | resolved | CMP0044 (C-2) |
| 16 | inherited test, member only on derived, no MemberType (declaring-class lookup) | n/a on JIT (JIT looks on derived) | CMP0051 not found (C-1) |
| 17 | member on the class itself | resolved | valid |
| 18 | static property on an interface MemberType | resolved | valid |
| 19 | own protected static | D2 | CMP0052 |
| 20 | own internal static | D2 | CMP0052 |
| 21 | base static property, derived instance field | base static | valid; call target = base |
| 22 | base private static | D1 | CMP0051 not found |
| 23 | base internal static | D2 | CMP0052 |
| 24 | base protected static | D2 | CMP0052 |
| 25 | base private instance | D1 | CMP0051 not found |
| 26 | base public instance | D2 | CMP0052 |
| 27 | base Cases<T>(), derived new Cases() | derived; valid | valid (derived) |
| 28 | base static property, derived static field | derived field | valid (derived field) |
| 29 | base Cases(int) only | D3 | CMP0052 requires arguments |
Names: ReflectedType.FullName is the derived class for an inherited test method, while DeclaringType is the base. A nested type's FullName uses +. A closed generic's FullName is assembly-qualified with the runtime core library, which Roslyn can't reproduce, hence B4.3.
ADR consistency: C-1's derived-only-member case and C-2's holder accessibility are product-owner-approved AOT-only rejections. ADR-0070 item 13 says valid usage is never rejected on one engine only, so the product owner approved and recorded them as ADR-0070 Amendment 2 (2026-11-06). They are the only AOT-only exceptions. The C-4 skip residual is an invalid-usage timing difference, already covered by item 13. No new public API, no core change, no architectural redesign. The diagnostic IDs fall inside the ADR's approved CMP0051+ range.
-
2026-11-06 (B4 implementation,
compono-lsx.4): B4 (AOTComposeDatadiscovery + validation) is implemented and all 16 acceptance-checklist items pass. Implementation:src/Compono.Generators/Models/AotComposeDataMethodInfo.cs(equatable model, no retained Roslyn symbols/locations);src/Compono.Generators/Discovery/AotComposeDataMethodDiscovery.cs(member resolution B4.4, return/element shapes B4.5-B4.6, typed-row assignability B4.7, profile checks B4.8, active-only skip B4.9, duplicate-[Shared]B4.10, precedence B4.11, diagnostics CMP0051-CMP0057 + ComposeData twins);ComposeMethodDiscoveryAOT-ComposeData metadata-name consts;ComponoIncrementalGeneratorraw plan providers (3) folded intocomposeMethodResultsAll(so parameter plans are emitted even for invalid methods) plus a validated per-method pipeline whoseRegisterSourceOutputreports diagnostics only (B4 emits no registration);AotComposeMethodDiscoveryCMP0045 extended to all six Compose-family forms;DiagnosticDescriptorsCMP0051-CMP0057 + ComposeData twin descriptors (existing[Compose]descriptors and snapshots unchanged);AnalyzerReleases.Unshipped.mdupdated. Also corrected a B2 public-API omission discovered during B4: added public sealedCompono.XunitV3.Aot.SharedAttribute(same contract as the JIT marker), recorded as ADR-0070 Amendment 3, with the AOT public-API surface test, a focused marker contract test, and the package docs updated. Tests: newtest/Compono.Generators.Tests/AotComposeData*files (member-resolution probe rows 01-29, shapes/assignability, profile, diagnostics/CMP0045/skip/CMP0044/precedence, model/metadata, plan pipeline, incremental caching) - 117/117 pass. Full solutiondotnet build: 0 warnings/0 errors. Full solutiondotnet test: 5153 total, 0 failed, 68 skipped. B4 (compono-lsx.4) is complete but not yet closed, pending independent Claude Opus review. B5 (compono-lsx.5) is next and owns AOT[Shared]execution and the emission of the ComposeData registration. -
2026-11-06 (B4 independent-review remediation,
compono-lsx.4): The independent review returned request-changes; the blocking findings and test-strengthening asks were addressed before the second review. B-1: the ComposeData parameter loop now rejects any parameter whose type failsComposedTypeAnalyzer.IsRowInvokerShapeEligible(open generic parameter, ref struct, pointer, function pointer, unsupported array shape) with the CMP0040 signature rule, so an unsupported parameter can no longer produce a zero-diagnostic model. B-2:DiagnosticInfovalue equality/hash now also consider the descriptor's message format, so the ComposeData twin descriptors that share a rule id but differ in wording (e.g.CMP0051not-found vs ambiguous,CMP0052not-public-static vs requires-arguments vs generic-method) no longer compare equal - the incremental driver now re-reports correctly (unit equality test plus an incremental run test where a not-found member becomes ambiguous). B-3: the skip-condition check replaced the "any static public bool property" boolean with a most-derived-by-name resolver plus a public-static-getter/bool validity check, and records the resolved property's containing type. A private getter, a setter-only property, or a derived instance member hiding a base static property now reportsCMP0057; a valid inherited public static bool property is recorded with its (base) containing type. B-4: member-resolution property hiding now matches reflection hide-by-name-and-signature across static properties of identical type regardless of accessibility (a non-public derived static property of identical type hides a public base property ->CMP0052; a different type leaves the base selectable). B-5:docs/packages/compono-xunitv3-aot.mdcorrected - B4 compile-time discovery/validation is implemented and reports the diagnostics, while B5 execution emission is not, so a[ComposeData]theory still does not execute under AOT. Inv1: duplicate-[Shared]type identity now normalizes CLR-equivalentdynamic/objectand top-level tuple element names before grouping. Inv2: a mixed[Compose]+[ComposeData]stack now reports the six-formCMP0045wording from both pipelines while Compose-only stacks keep the existing three-form text and snapshots. Inv3: fixed theCMP0040double period (the duplicate-[Shared]reason no longer ends with a period); the threeCMP0052messages keep no trailing period because RS1032 (enforced) requires a single-sentence message to have no trailing period - the PLAN's periods on those single-sentence messages are incompatible with RS1032. Test strengthening:ComposeDataAotTestHelpernow fails on unexpected compiler errors, which surfaced that the existing ComposeData test sources were not valid C# (file-scoped test namespace after the block-scoped stand-ins); the six test helpers were made block-scoped withusing Compono;. Newtest/Compono.Generators.Tests/AotComposeDataRemediationTests.cscovers B-1..B-4 and Inv1..Inv3. Post-remediation validation: full solutiondotnet build0 warnings/0 errors; full solutiondotnet test5197 total, 0 failed, 68 skipped (Compono.Generators.Tests1016 = 508 per TFM, no Verify snapshot changes);dotnet pack src/Compono.XunitV3.Aotsucceeds with the additiveCompono.XunitV3.Aot.SharedAttribute. B4 remains open pending the second independent review. -
2026-11-06 (B4 second-review remediation,
compono-lsx.4): The second independent review accepted B-1/B-2/B-3/B-5 and asked for a narrowly scoped remediation; B-4 was over-corrected and has been re-scoped. C-1: member-resolution property hiding no longer lets an inherited private static property participate - a private property participates only when declared on the lookup type itself (where it can still causeCMP0052); internal/protected/base-public static properties keep the reflection-probe behavior, and differing types still leave the base selectable. C-2: duplicate-[Shared]detection is now a direct pairwise check usingCompilation.ClassifyConversion(a.Type, b.Type).IsIdentity(the customNormalizeSharedTypeIdentitygrouping was removed). This matches CLR identity: nullable-reference annotations,dynamicvsobject, and tuple element names are erased, whileBox<int?>stays distinct fromBox<int>. C-3 (recorded here): the plan'sCMP0052diagnostic texts no longer show a trailing period (RS1032); the B4.10 duplicate-[Shared]rule now states the CLR-identity comparison (preserving the nullable-reference vs nullable-value distinction); and anRS1032diagnostic-message note was added under the diagnostics table. ADR-0070 was not modified. Tests: added the C-1 three-levelG/M/Lhierarchy tests and the C-2 CLR-identityBox<...>theory (plus the corrected, renamed dynamic-parameterCMP0040test); strengthened probe rows 02/08/16/22/25 to assert the ambiguous/not-found message variant; added profile-twin wording tests (incl. the<TProfile>-onlyCMP0044), aCMP0056too-many-values-with-TConfigtest, adjacent-precedence tests asserting the winning message, and an incremental diagnostic test that edits a separate holder source file while keeping the method location stable. B4 remains open pending the third independent review. -
2026-11-06 (B4 third-review remediation,
compono-lsx.4): D-1 (property hiding must use CLR type identity) fixed:AotComposeDataMethodDiscovery.ResolveMembernow takes theCompilationand compares candidate property signatures withcompilation.ClassifyConversion(existing, candidate).IsIdentityinstead ofSymbolEqualityComparer.Default. This makesIEnumerable<dynamic>hideIEnumerable<object>and keeps tuple-element-name differences (List<(int a, int b)>vsList<(int c, int d)>) as the same CLR signature, so a derived property hides an identical-CLR base property. Preserved behavior: private properties participate in hiding only on the lookup type, inherited private does not participate, internal/protected and public-base are unchanged, differing property types still leave the base selectable, and the field/property/method precedence is untouched. Regression tests added (probe rows 30 and 31) asserting the resolved member's containing type, not merely the absence of diagnostics. Also added the missing precedence tests asserting the winning message:CMP0040> profile validation, profile validation >CMP0054,CMP0044(holder) >CMP0053,CMP0053>CMP0055,CMP0056>CMP0057; strengthened theComposeDataprofile-twin wording tests forCMP0046/CMP0047/CMP0048/CMP0049; and corrected the diagnostics-table trailing periods forCMP0055/CMP0056/CMP0057(RS1032). ADR-0070 was not modified. B4 received independent approval (all 16 acceptance criteria satisfied);compono-lsx.4is closed, with B5 (compono-lsx.5) owning the generatedComposeDataexecution. Final checkpoint validation: full solutiondotnet build0 warnings/0 errors; full solutiondotnet test5,255 total tests / 5,187 passed / 68 skipped / 0 failed;dotnet pack src/Compono.XunitV3.Aot -c Releasesucceeds; no Verify snapshot changes. -
2026-11-06 (B5 specification,
compono-lsx.5, design owner): a corrected B5 specification and ADR-0070 Amendment 4 replace the uncommitted draft OpenCode/DeepSeek wrote earlier the same day. That draft was a proposal, not an approved specification. It said "approved by the maintainer" and "approved after the independent Opus architecture review", and neither was true: the review ended at REQUEST CHANGES with open decisions. Amendment 4 is Proposed, pending product-owner approval. B5 must not start before that approval. - Evidence: the two B5 architecture-review rounds (2026-11-06). They used hand-written B5-shaped registrations against
Compono.XunitV3.Aotat93c8c40and xUnit AOT 4.0.¼.0.2, run under the JIT host and under a real Native AOT publish (0 IL/AOT warnings:[ComposeData<TProfile, TConfig>]+[Shared]rows passed; attributeSkip+SkipUnlessrows skipped), plus decompiledxunit.v3.core.aot4.0.2. Re-checked during specification:RegisteredEngineConfig.GetTheoryDataRowFactories(ICodeGenTestMethod)is public and keys onDeclaredTypeIndex;ICoreTestClass.Classis public;test/Compono.XunitV3.Aot.Testsdoesn't reference NSubstitute; #166 is still OPEN with no ADR or plan. - Probe findings the specification relies on:
- xUnit's index for inherited methods on generic bases is the open form (
<>,<,>, nested forms; B5.3 table), andtypeofof that text compiles. TestContext.Current.TestClassis the concrete derived class inside the factory under Native AOT, and walkingBaseTypegives the exact closed base.- A member on
GenericBase<T>(default lookup) isCS0246from generated code but runs on JIT. - B4 compared
TConfigconstructors by symbol equality:CMP0042for(int A, int B),dynamic, andList<dynamic>constructors that JIT accepts. - A static virtual interface member gives
CS8926from generated code but runs on JIT; a static abstract one fails on both engines. [Obsolete(error: true)]/[Experimental]data members are uncompilable from generated code.- JIT's two async enumeration paths differ (B5.9).
- xUnit 4.x AOT only checks
DisableParallelization == true.
- xUnit's index for inherited methods on generic bases is the open form (
- Corrections to the draft:
- It claimed approvals that didn't exist (removed).
- It said
CMP0047was the only new AOT-only exception. Wrong: the open-generic member/skip holder, static virtual interface members, the B4CMP0040rejection of type-parameter parameters on generic bases, and multipleIAsyncEnumerable<T>implementations are also AOT-only (Amendment 4 §2a–e). - It gave
MemberReadFailureanattributeSeedparameter and a "pre-row seed line". JIT member-level failures have no seed line, and the real contract unwraps oneTargetInvocationException(B5.5). - It gave
NegativeSeeda single row-context shape. JIT has two distinct texts: an attribute-level one with a pre-row seed line, and a per-row R0 with a row seed line. NowNegativeAttributeSeedandNegativeRowSeed. - It said "exactly two helpers" while also requiring a third (
ResolveExecutingDeclaringType). The surface is now four hidden members, all listed in Amendment 4 §3. - It allowed an open-generic
typeoffallback. That fallback is rejected: it changes custom providers'DeclaringType, diagnostics, and{T}. - It said "don't emit for open generic test classes". Wrong: the generic base is the registration key, so always register.
- It named the value-type async element type. It's now inferred.
- It left out keyword escaping, the null-
TaskD5 rule,CS0162on a negative attribute seed, plain-[Obsolete]CS0618underTreatWarningsAsErrors, inaccessible test classes,DisableParallelization ?? false, a concrete way to run failure-path tests (B5.10 harness), and slice-level real-runner gates. - It cited
ConstructUnboundGenericType()for the index, which isn't defined for a non-generic type nested in a generic one. It's replaced by a specified renderer. - It said "no new public API". The four helper members are hidden public API on AOT and need the amendment.
- Kept from the draft: support for generic bases with per-execution closed roots; the
TConfigidentity fix; theCMP0047/CMP0052/CMP0054twins (with corrected scope and text); both async paths; no[Compose]changes; follow-ups recorded. - Decisions requiring product-owner approval: Amendment 4 as a whole. In particular:
- (a) the closed-root type-identity read under ADR-0001 (recommended), versus rejecting generic bases on AOT entirely;
- (b) the four hidden helper members (recommended), versus duplicating literals in generated code;
- © reproducing both JIT async paths (recommended), versus normalizing JIT, which would change B3 behavior.
- Follow-ups (not B5; filing them needs product-owner approval):
- AOT
[Compose]has the same generic-declaring-type rendering defect and the same symbol-equalityTConfigmatch (AotComposeMethodDiscovery). - A
CMP0046-style check forRequiresUnreferencedCode/RequiresDynamicCodeon the data member. - Holder-type or assembly-level
[Obsolete(error: true)]/[Experimental]exposure, which existing AOT[Compose]shares.
- AOT
-
#166 checkpoint: B5 owns the seeded-row scaffold (B5.1). Re-check when S2 starts.
-
2026-11-06 (B5 maintainer decisions and §2e investigation,
compono-lsx.5): the product owner approved ADR-0070 Amendment 4 in principle: - runtime closed-generic-base resolution (consistent with ADR-0001; no open-generic fallback);
- the four hidden
ComposeDataRowshelpers; - preserving both JIT async enumeration paths and their exception semantics;
- the
TConfigCLR-identity fix; - exact diagnostic contracts with real generated-code compilation and execution;
- filing separate follow-up issues for the AOT
[Compose]generic-base/TConfigparity defects and forRequiresUnreferencedCode/RequiresDynamicCodecall-site diagnostics.
§2e (multiple IAsyncEnumerable<T>) was conditional pending investigation. - How B3 JIT chooses: - TryGetAsyncEnumerableElementType takes the declared type itself if it is IAsyncEnumerable<>, otherwise the first match in Type.GetInterfaces(). - AppendAsync then branches on the runtime object: source is IAsyncEnumerable<object?> → await foreach fast path; otherwise the reflection loop over the selected element type. - CoreCLR probe (net10.0, scratch console app with explicit implementations of each pair):
| Type | `GetInterfaces()` order | `is IAsyncEnumerable<object?>` | Rows read |
|---|---|---|---|
| `IntThenString` / `StringThenInt` | declaration | true | `string` both ways |
| `IntThenLong` / `LongThenInt` | declaration | false | reflection on the first: `int` vs `long` |
| `StringThenUri` / `UriThenString` | declaration | true | `string` vs `Uri` (first in the interface map) |
| `ObjectThenString` / `StringThenObject` | declaration | true | exact `object` implementation both ways |
- Conclusion: one reference element plus value elements is deterministic and is now supported: AOT uses the same
IAsyncEnumerable<object?>cast, with no reflection and no named element type. Two or more reference elements, or two or more value elements with none of reference type, depend on undocumented runtime ordering or variance resolution. So they stayCMP0053, justified in Amendment 4 §2e as a correctness requirement. Native AOT runtime behavior for the ambiguous cases wasn't probed, and the rejection doesn't depend on it. - Spec changes:
- B5.2:
AsyncRowElementIsValueTypedefinition. - B5.4: the R/V async-interface table, plus S0 tests.
- B5.6: the value-element reader now performs JIT's runtime
IAsyncEnumerable<object?>check first. This is a parity gap found during the investigation: JIT decides by the runtime object, so a value-element declared type whose runtime object also implements a referenceIAsyncEnumerable<T>reads the reference rows. - B5.9: a path-selection row.
- S4: path-selection tests.
- B6: documenting and parity tests.
- B5.2:
- Editorial pass:
- Marked suggested structure as distinct from normative behavior (B5 status paragraph, row readers).
- Removed the speculative
ICollectionpre-sizing instruction. - Amendment 4 status updated, the ADR-0001 confirmation recorded, and the §6 follow-ups marked approved.
-
Open: the product owner's confirmation of the narrowed §2e before S0. The follow-up issues are approved but not yet filed; the design owner was limited to editing ADR-0070 and PLAN-0071.
-
2026-11-06 (B5 design approval gate,
compono-lsx.5): the product owner gave final approval of ADR-0070 Amendment 4, including the narrowed §2e: - supported: one reference-type
IAsyncEnumerable<T>plus any number of value-type ones; CMP0053: two or more reference-type implementations, or two or more value-type implementations with no reference-type one;- preserved: JIT's runtime-object path selection, including the
IAsyncEnumerable<object?>check before the value-type reader.
The B5 architecture and this specification are approved for implementation. The design approval gate has passed and B5 S0 is authorized. B5 implementation hasn't started. - The specification is committed as a documentation-only checkpoint. - The three approved follow-up issues (Amendment 4 §6) are still to be filed.
- 2026-11-06 (B5 S0 checkpoint,
compono-lsx.5): S0 (B5.2 + B5.4) is committed at5db86fc("feat(xunitv3): add AOT ComposeData S0 discovery support"), whose parent ise709228. The design owner verified it independently; the product owner approved it. - Scope: discovery only — the B5.2 corrections/model additions, the B5.4 diagnostics, and the B5.3 index renderer. No emission. Twelve files.
src/Compono.Generators/Discovery/AotComposeDataMethodDiscovery.cs: the B5.2TConfigconstructor-identity fix (SymbolEqualityComparer.Default.Equals(…)replaced bycompilation.ClassifyConversion(…).IsIdentity); the new B5.4 checks — open-generic member/skip holder, static abstract/virtual interface member, prohibited member/getter/skip property/skip getter, and the multiple-IAsyncEnumerable<T>set — at the B5.4 precedence; and population of the new model fields.src/Compono.Generators/Discovery/AotComposeDataTestClassIndex.cs(new): the B5.3 index renderer (Render(INamedTypeSymbol)), rendering each containing type's own name with open-generic arity markers (<>,<,>) andFullyQualifiedFormatkeyword escaping.src/Compono.Generators/Discovery/ComposedTypeAnalyzer.cs:ContainsTypeParametermadeinternalfor reuse by the open-generic check (no behavior change).src/Compono.Generators/Models/AotComposeDataMethodInfo.cs: five new fields —TestClassIndex,IsGenericTestClass,IsTestClassAccessible,LookupIsTestClass, andAsyncRowElementIsValueType.src/Compono.Generators/Diagnostics/DiagnosticDescriptors.cs: seven newComposeDatatwin descriptors —ComposeDataOpenGenericMemberHolderandComposeDataOpenGenericSkipHolder(CMP0054),ComposeDataStaticInterfaceMember(CMP0052), andComposeDataProhibitedMember/ComposeDataProhibitedMemberGetter/ComposeDataProhibitedSkipProperty/ComposeDataProhibitedSkipPropertyGetter(CMP0047). The multiple-async-interface condition reuses the existingComposeDataUnsupportedReturnOrRowElementTypedescriptor (CMP0053). Existing descriptors, their messages, and their Verify snapshots are unchanged.src/Compono.Generators/AnalyzerReleases.Unshipped.md: theCMP0047/CMP0052/CMP0053/CMP0054notes extended for the newComposeDataconditions (keyed by ID, so no new rows).- Tests: new
test/Compono.Generators.Tests/AotComposeDataAmendment4DiagnosticTests.cs,AotComposeDataAmendment4ModelTests.cs,AotComposeDataAmendment4TConfigIdentityTests.cs,AotComposeDataTestClassIndexTests.cs, andCompilerFeatureRequiredReference.cs, plus additions toComposeDataAotTestHelper.cs.
- Approved scope: B5.2 + B5.4 exactly. No B5 emission (no template, emitter, wiring, or generated registration) and no S1-or-later work.
- Review verdict: independent Claude Opus review returned APPROVE.
- G1: full-solution
dotnet build, 0 warnings, 0 errors. - G2: full-solution
dotnet test, 5385 total, 0 failed, 5317 passed, 68 skipped. The 68 skips are pre-existing. The working tree was clean after the run, so no*.verified.*changed (the existing generator snapshots are unchanged) and the AOT[Compose]files are untouched. - Not in S0's commit (by design): ADR, plan, bead, and snapshot changes. This entry is S0's documentation closeout.
-
Open: S1 (
4b9d12e) is committed next and recorded below. S2 (bb67950) followed it (see the S2 checkpoint below); the #166 re-check (B5.1) happened at S2 start. -
2026-11-07 (B5 S1 checkpoint,
compono-lsx.5): S1 (B5.5) is committed at4b9d12e("feat(xunitv3): add shared ComposeData AOT helpers"), whose parent is the S0 commit5db86fc. The design owner verified it independently; the product owner approved it. - Scope: exactly four files.
src/Shared/Compono.XunitV3/ComposeDataRows.cs:MemberReadFailure,NegativeAttributeSeed,NegativeRowSeed, and the AOT-onlyResolveGenericTestClassType. Each matches the B5.5 contract text exactly, isinternalon JIT, and ispublic+[EditorBrowsable(Never)]on AOT. No other helper was added.src/Compono.XunitV3/ComposeDataAttribute.cs: the JIT switch-over only. The attribute- and row-level negative-seed checks call the shared helpers, the privateMemberReadFailureis deleted, and all of its callers use the shared one. The message texts are moved verbatim.test/Compono.XunitV3.Tests/ComposeDataRowsTests.cs: additions only, covering all three exact texts, theTargetInvocationExceptionunwrap, preservedInnerException/Diagnostic, no seed line onMemberReadFailure,NegativeAttributeSeed(-1)ending in"\n\nSeed: -1", andNegativeRowSeedwith and without a label.test/Compono.XunitV3.Aot.Tests/ComposeDataRowsTests.cs:ResolveGenericTestClassTypefrom closed<int>and<string>subclasses, from a multi-level subclass, and from a non-generic class (exactInvalidOperationExceptiontext). It also checks that the four helpers are public, static, and hidden, and that every declared public member is hidden.
- Public surface:
PublicApiSurfaceTestslocks public types only. S1 adds members, not types, so its expected list is correctly unchanged. The member-level lock for the four helpers is in the AOTComposeDataRowsTests. - JIT suite unchanged: no existing JIT test was edited, and the exact-text assertions that existed before S1 pass. That is the byte-parity proof.
- G1: full-solution
dotnet build, 0 warnings, 0 errors. - G2: full-solution
dotnet test, 5433 total, 0 failed, 5365 passed, 68 skipped. The skips areCompono.XunitV3.Tests' 17 per TFM, all predating S1, which added none.Compono.XunitV3.Aot.Testspassed 42 on each TFM. The working tree was clean after the run, so no*.verified.*changed, and the AOT[Compose]files are untouched. - ApiCompat:
dotnet pack -c ReleaseofCompono.XunitV3andCompono.XunitV3.Aotwith-p:Version=1.3.2 -p:PackageValidationBaselineVersion=1.3.2(the nuget.org baseline) gave no warnings or errors, so the change is additive only. - Not in S1's commit (by design): ADR, plan, bead, and snapshot changes. This entry is S1's documentation closeout.
-
Open: S2 (
bb67950) followed and is recorded below; the #166 re-check (B5.1) happened at S2 start. -
2026-11-07 (B5 S2 checkpoint,
compono-lsx.5): S2 (B5.6 wiring and template) is committed atbb67950("feat(xunitv3): generate AOT ComposeData registrations"), whose parent is the S0 documentation checkpoint3d4bd28. The design owner (Claude Opus) verified it independently and returned APPROVE; the product owner approved it. - Scope: B5.6 only - the generated AOT
ComposeDataregistration template, emitter, and generator wiring, plus its tests. 31 files changed (29 added, including 23 new Verify snapshots; 2 modified), covering only the S2 envelope: a non-generic test class; plain[ComposeData]; theSyncmember shape (field, property, method; inherited member;MemberType); untyped and typed (TheoryData<…>) rows; B5.7 without a profile; B5.8 with constants only; theCompono.Seedtrait; and R0, too-many, and null/type-mismatch row failures (the profile-driven negative row seed is S3). No generic bases, noTaskOf/ValueTaskOf/AsyncEnumerableshapes, and no profile/seed/[Shared]/active-skip features.src/Compono.Generators/Templates/AotComposeDataRegistration.scriban(new): the registration template - header/pragmas, thefileclass, the[ModuleInitializer]call, the per-shape row readers, the seeded-row scaffoldComposeRow, parameter binding, and the metadata block.src/Compono.Generators/Emitters/AotComposeDataRegistrationEmitter.cs(new): the view-model builder and the S2 emission gateIsWithinS2Envelope(renamed toIsWithinEmissionEnvelopein S3,0680dc2- see the S3 checkpoint below).src/Compono.Generators/ComponoIncrementalGenerator.cs: the B5.6 wiring - after the existing diagnostics step, skip whenDiagnostics.Count > 0 || !IsTestClassAccessible, thenEmissionIsolation.TryEmit(so an emission failure surfaces asCMP0050for that method only, ADR-0068).test/Compono.Generators.Tests/AotComposeDataRegistrationEmissionTests.cs(new, 14 tests): the S2 envelope - an eligible sync non-generic shape emits exactly one registration; every out-of-envelope shape (profile forms, seed,[Shared], active skip, generic base, async shapes, inaccessible class) emits none and reports no new diagnostic.test/Compono.Generators.Tests/AotComposeDataRegistrationVerifyTests.cs(new, 10 tests) plusGeneratorTestHelpers.cs(VerifyGeneratedSource): 23 new snapshots.test/Compono.XunitV3.Aot.Tests/Support/RegisteredComposeDataFactory.csandComposeDataEmissionTests.cs(new): the B5.10 harness and the real-runner S2 tests.
- Claude Opus independent-review correction round (F1/F2/F4/F5). The review requested changes; the correction round addressed F1, F2, F4, and F5, including making two eligibility tests non-vacuous (each now genuinely fails when the S2 envelope predicate is widened or removed, instead of passing without exercising the intended rejection).
- G1 (DeepSeek-reported full-suite result): full-solution
dotnet build, 0 warnings, 0 errors. - G2 (DeepSeek-reported full-suite result): full-solution
dotnet test, 5,544 total, 5,476 passed, 68 skipped, 0 failed. The working tree was clean after the run, so no unexpected*.verified.*changed and the AOT[Compose]files are untouched. - G3 (Claude Opus independently verified focused tests): 14 emission tests, 10 snapshot tests, and 23 new snapshots, with no unexpected
CMP0050. - G4 (Claude Opus independently verified): the generated code compiles against the real
Compono,Compono.XunitV3.Aot, and xUnit AOT assemblies on net9.0, net10.0, and net11.0. - G5 (Claude Opus independently verified real-runner probes): the slice's tests run and pass under the real xUnit AOT runner, 63/63 per TFM.
- Emission gate: S2 emits only the
IsWithinS2Envelopeshape; every emit path gates on that single predicate. S3-S6 must each explicitly widenIsWithinS2Envelopeas they implement their own functionality. (Historical: S3 renamed this predicate toIsWithinEmissionEnvelopeand widened it for the S3 forms - see the S3 checkpoint below; the pre-widening S2 shape/name is what is recorded here.) - #166 checkpoint (B5.1), re-checked at S2 start: issue #166 was still OPEN (last updated 2026-11-04), with no accepted ADR and no implementation plan, so B5 keeps ownership of the seeded-row scaffold (B5.7) and no S2 design change was required. Issue #166 was not modified.
- Not in S2's commit (by design): ADR, plan, bead, and snapshot-list changes. This entry is S2's documentation closeout.
-
Open: S3-S7 are pending; B5 remains in progress.
-
2026-11-09 (B5 S3 checkpoint,
compono-lsx.5): S3 (B5.7-B5.9 profile, seed, and[Shared]support) is committed at0680dc2("feat(xunitv3): support AOT ComposeData profiles and seeds"), whose parent is the S2 documentation checkpoint7c52b0e. The design owner (Claude Opus) independently re-reviewed after a correction round and returned final APPROVE; the maintainer authorized the checkpoint commit. - Scope: 20 files changed (6 modified, 14 new Verify snapshots; 1,894 insertions, 49 deletions), covering the S3 envelope only.
src/Compono.Generators/Emitters/AotComposeDataRegistrationEmitter.cs: the emission gate renamed and widened (see below); view-model additions (AttributeSeedExpression,HasNegativeAttributeSeed,HasConfig,ConfigTypeName,ComposerCreateExpression, per-parameterIsShared).src/Compono.Generators/Templates/AotComposeDataRegistration.scriban: per-row profile/config construction, config extraction (ExtractConfig<TConfig>), attribute-seed application (WithSeedafterAddProfile), the negative-seed whole-body throw, and shared-first parameter resolution (ShareExplicit/ResolveSharedbefore ordinaryResolve).src/Compono.Generators/ComponoIncrementalGenerator.cs: the wiring call updated to the renamed predicate.test/Compono.Generators.Tests/AotComposeDataRegistrationEmissionTests.cs: five former S2 no-emit tests converted to positive S3 emission tests; no-emit retained for still-out-of-envelope shapes.test/Compono.Generators.Tests/AotComposeDataRegistrationVerifyTests.cs: five new snapshot tests (profile, profile-config+seed, seed-after-profile, negative seed, shared value/branch).test/Compono.XunitV3.Aot.Tests/ComposeDataEmissionTests.cs: real-runner S3 success and failure coverage.
- S3 features implemented:
<TProfile>(fresh profile instance per row,AddProfile<TProfile>()) and<TProfile, TConfig>(freshTProfile(rowConfig)per row); attributeSeed(positive,0treated as a real configured seed, and a negative seed whose whole factory body throwsNegativeAttributeSeed); effective-seed precedence (attributeSeedpins every row, else profileWithSeed, else a fresh non-negative row seed; attributeSeed+ profileWithSeedsurfaces the core duplicate-configuration conflict as a wrapped row failure);[Shared]parameters resolved shared-first with row-value →ShareExplicit, elseResolveShared, and an ordinary parameter of the same type reusing the shared instance; per-row composition independence (freshComposer, per-row shared scope, profile constructed once per row); negative-seed and failure propagation with the approvedSeed:context lines and preservedInnerException/Diagnostic. - Initial review findings and resolution (all seven resolved before re-review): (1) shared-parameter ordering when an ordinary parameter precedes a
[Shared]parameter of the same type - added a real-runnerAssert.Samewitness and a snapshot proving shared-first emission; (2) the three S0TConfigidentity shapes (tuple, dynamic/object,List<dynamic>/List<object>) - added real generated-registration coverage; (3) attributeSeed = 0- added a runtime trait assertion and aWithSeed(0)snapshot; (4) seed failure context - added exact-message tests provingSeed: 42on both a before-composition failure (config extraction) and an after-row-creation failure (positional mismatch); (5) throwing profile constructors - added ordinary-exception and diagnosed-CompositionExceptioncases asserting preservedInnerException/Diagnosticand the pre-composition seed line; (6) per-row independence - added tests proving[Shared]instances are not reused across rows and that the profile constructor runs once per row (distinct instances); (7) low-severity cleanup - the negative-seed test now uses a member that throws if read (provingNegativeAttributeSeedwins without evaluating it), and the emitter's trailing newline was restored. - Emission envelope rename:
IsWithinS2Envelope→IsWithinEmissionEnvelope(single authoritative emission-eligibility predicate). It now admits the S3 profile/config,Seed, and[Shared]forms while retaining the S4-S6 exclusions. The forward-looking gate text above and the S2 checkpoint Notes were annotated with the rename; S2's pre-widening shape/name is preserved as historical evidence. - G1 (DeepSeek-reported full-suite result): full-solution
dotnet build, 0 warnings, 0 errors. - G2 (DeepSeek-reported full-suite result): full-solution
dotnet test, 5,621 total, 5,553 passed, 68 skipped, 0 failed. The 68 skips are pre-existing; the tree was clean after the run, so no*.verified.*changed and the AOT[Compose]files are untouched. - G3 (DeepSeek-reported, Claude independently verified): generator
AotComposeDatasuite 526/526, with no unexpectedCMP0050. - G4 (Claude independently verified): the generated registrations compile against the real
Compono,Compono.XunitV3.Aot, and xUnit AOT assemblies on net9.0, net10.0, and net11.0. - G5 (DeepSeek-reported, Claude independently verified): the real xUnit AOT runner executes the slice's tests - 126/126
ComposeDataEmissiontests across the three TFMs. - Claude independently verified (full focused suites):
test/Compono.XunitV3.Aot.Tests252/252; generator test project 1,264/1,264; and no existing verified snapshot changed. - Not in S3's commit (by design): ADR, plan, bead, and snapshot-list changes. This entry is S3's documentation closeout.
-
Open: S4-S7 are pending; B5 remains in progress.
-
2026-11-09 (B5 S4 checkpoint,
compono-lsx.5): S4 (B5.6-B5.9 asynchronous member sources) is committed at3fcc5d4("feat(xunitv3): support async AOT ComposeData sources"), whose immediate Git parent is the S3 documentation checkpointe721e33("docs(xunitv3): record ComposeData S3 checkpoint"); the S3 implementation commit is0680dc2. The design owner (Claude Opus) independently reviewed after a correction round and returned final APPROVE; the maintainer authorized the checkpoint commit. This entry is S4's documentation closeout. - Scope: 21 files changed (5 modified, 16 new Verify snapshots; 882 insertions, 29 deletions on the five source/test files before snapshot acceptance), covering the S4 envelope only.
src/Compono.Generators/Emitters/AotComposeDataRegistrationEmitter.cs:IsWithinEmissionEnvelopewidened to admit all four return shapes (still excluding a nullReturnShape/MemberKind, generic test classes, and activeSkipUnless/SkipWhen); view-model additionsIsSync/IsTaskOf/IsValueTaskOf/IsAsyncEnumerable/IsAsyncEnumerableValueElement(the last =AsyncEnumerableshape with a value-type row element).src/Compono.Generators/Templates/AotComposeDataRegistration.scriban: the row-reader selection (ReadRowsforSync,await ReadRowsAsyncotherwise), theTaskOf/ValueTaskOfawaited reader (with theTaskOfnull-Taskcheck → D5, no null check forValueTaskOf), theAsyncEnumerablereference-element reader (await foreachover theIAsyncEnumerable<object?>covariance fast path), and the value-element reader (ReadValueRowsAsync<TElement>, element type inferred) that first performs the runtimeIAsyncEnumerable<object?>check before the manual enumerator loop.test/Compono.Generators.Tests/AotComposeDataRegistrationEmissionTests.cs: three former async no-emit tests converted to positive emission tests, plus newTask<object>and value-element emission coverage.test/Compono.Generators.Tests/AotComposeDataRegistrationVerifyTests.cs: seven new snapshot tests.test/Compono.XunitV3.Aot.Tests/ComposeDataEmissionTests.cs: real-runner S4 success and failure coverage.
- S4 features implemented:
Task<T>member sources (including a nullTask→ D5, never awaited);ValueTask<T>member sources;IAsyncEnumerable<T>member sources on both the reference-element and value-element paths; runtime-covariance path selection (a declared value-element source whose runtime object also implements a referenceIAsyncEnumerable<T>reads the reference rows, matching JIT); full source enumeration/materialization before any per-row composition; preservation of profiles, config extraction,Seed(attribute/profile/row),[Shared], row metadata, and per-row isolation; the B5.9 exception/cancellation filter matrix (a member that throwsCompositionExceptionwhen invoked → wrapped; thrown while awaiting → passes through unwrapped; thrown at a reference-pathGetAsyncEnumerator→ passes through; at a value-pathGetAsyncEnumerator→ wrapped;DisposeAsyncfailure precedence: reference path replaces the original failure, value path keeps the original); andOperationCanceledExceptionpropagation on both async enumeration paths.ConfigureAwait(false)on every emitted await; no runtime reflection and no LINQ in generated code. - Emission envelope widening:
IsWithinEmissionEnvelope(renamed fromIsWithinS2Envelopein S3) is the single authoritative emission-eligibility predicate. S4 widened it to admit theTaskOf/ValueTaskOf/AsyncEnumerable(reference- and value-element) shapes while retaining the S5 active-skip and S6 generic-test-class exclusions and the zero-diagnostic/accessibility gates and per-methodCMP0050EmissionIsolation. - Review findings and resolution (all addressed before approval): (1) awaited
CompositionExceptionforTask<T>andValueTask<T>passes through unwrapped - added real-runner tests asserting message, preservedDiagnostic, and nullInnerException; (2)GetAsyncEnumeratorexceptions - reference pathCompositionExceptionpasses through, value path wraps per B5.9, proven with custom enumerables that throw atGetAsyncEnumerator; (3)DisposeAsyncfailure precedence on each path; (4) a static mixed source whose declared type implements both a value- and a reference-IAsyncEnumerableproves the reference reader is selected and reads valid rows; (5) a strengthened runtime-covariance test reading rows successfully through the reference interface; (6) restored explicitTask<object>emission coverage (kept alongsideTask<IEnumerable<object>>); (7) corrected two misleading emitter comments (no logic change). - G1 (DeepSeek-reported full-suite result): full-solution
dotnet build, 0 warnings, 0 errors. - G2 (DeepSeek-reported full-suite result, Claude independently re-ran): full-solution
dotnet test, 5,732 total, 5,664 passed, 68 skipped, 0 failed. The 68 skips are pre-existing; the tree was clean after the run, so no*.verified.*changed and the AOT[Compose]files are untouched. - G3 (DeepSeek-reported, Claude independently verified): generator test project 1,282/1,282 across net10.0/net11.0, with no unexpected
CMP0050; no existing verified snapshot modified. - G4 (DeepSeek-reported, Claude independently verified): the generated registrations compile against the real
Compono,Compono.XunitV3.Aot, and xUnit AOT assemblies on net9.0, net10.0, and net11.0. - G5 (DeepSeek-reported, Claude independently verified): the real xUnit AOT runner executes the slice's tests - 345/345 (115 per target framework).
- Claude independently ran the full build, the full suite, the generator tests, and the real AOT runner, and issued APPROVE.
- Known limitation - reflection wrapping (accepted, low severity, not an S4 blocker): JIT uses
MethodInfo.Invokefor member and value-path enumerator calls, so a synchronous exception may arrive wrapped in aTargetInvocationException; AOT direct calls expose the original exception to the exception filters. This can produce a different wrapper orInnerExceptionidentity for synchronousCompositionException/OperationCanceledExceptionfailures. Compiler-generated async methods and iterators generally surface failures through tasks and avoid the discrepancy. S4 intentionally changed neither JIT nor AOT production semantics. Recorded as a known remaining risk; no new implementation task was created and it was not assigned to S5. - Known limitation - cancellation (nonblocking test limitation): the S4 tests cover
OperationCanceledExceptionpropagation on both async enumeration paths, but they simulate cancellation by throwingOperationCanceledExceptionrather than exercising a real cancelledTestContexttoken. The per-rowThrowIfCancellationRequested()check is emitted according to the plan, but real-token observation remains untested and is not claimed to have been independently verified; it may be considered during B6 validation. - Not in S4's commit (by design): ADR, plan, bead, and snapshot-list changes. This entry is S4's documentation closeout. The documentation changes in this entry are not yet committed.
- Open: S5-S7 are pending; B5 remains in progress.
- 2026-11-09 (B5 S5 review and interface skip-holder design decision,
compono-lsx.5): S5 is implemented on top offc8fe54and is uncommitted. The design owner (Claude Opus) independently reviewed it and returned REQUEST CHANGES. The maintainer did not accept deferring finding F1 and decided its design the same day. S0–S4 checkpoint history and status are unchanged. - F1 (blocking): interface skip holders. An active
SkipUnless/SkipWhennaming astatic virtualorstatic abstractinterface property produced generated code failing withCS8926instead of a diagnostic. B4/S0 skip validation (ResolveSkipProperty/IsUsableSkipProperty) never checked for static interface members, and S5 made the shape emittable.- Investigation probes: real xUnit 4.0.1 JIT runner, with
[ComposeData]and native[Fact]: static virtualruns with its condition honored, including on a closed generic interface and on an interface that also carries an unimplementedstatic abstractstatic abstractfails at run time (BadImageFormatException)- base-interface and implementing-class holders fail with "Cannot find public static property" (xUnit1054 on
[Fact]) - plain static interface properties run
- xUnit's own AOT generator emits the same
CS8926for a native[Fact]with astatic virtualinterfaceSkipType. - Parity options evaluated:
- a constrained generic accessor:
CS8920for interfaces with unimplementedstatic abstractmembers, andTypeLoadExceptionat Native AOT run time on net9.0/net10.0 with 0 publish warnings [UnsafeAccessor]: worked on JIT and Native AOT on net9.0–net11.0, but needs a generic accessor class for generic interfaces and would be a new generated-code pattern
- Investigation probes: real xUnit 4.0.1 JIT runner, with
- Decision (product owner, 2026-11-09), recorded in ADR-0070 Amendment 4 §7:
- no
UnsafeAccessorsupport - plain static interface skip properties are supported through direct access
static virtual→CMP0052twin, an explicit AOT-only parity exception (§7a; the sixth in Amendment 4 §2's list)static abstract→ the sameCMP0052twin, not a parity exception, because JIT also fails it (§7b)- base-interface and implementing-class holders keep the existing
CMP0057 - specified in B5.4 (
ComposeDataStaticInterfaceSkipProperty, predicate, precedence 9b) and the B5.11 S5 row
- no
-
Required S5 test corrections (F2/F3, specified in the B5.11 S5 row):
DisableParallelization: rowfalseoverrides attributetrue- real-runner coverage for a skip property inherited from the test class's base class
- real-runner coverage for a
SkipTypethat inherits its property from a base class - real-runner coverage for a plain
[Obsolete]skip property under the project'sCS0612/CS0618warnings-as-errors
Each shape compiled and passed in a design-owner probe, so these are coverage gaps, not defects. -
AnalyzerReleases.Unshipped.md(implementation phase, not yet changed): the existingCMP0052row's note gains the skip-property condition. The rest of the row stays unchanged. Proposed note:A [ComposeData] member is not public and static, requires arguments, or is a generic method - ComposeData can only invoke a public static field, property, or parameterless non-generic method; also covers a selected member, or an active SkipUnless/SkipWhen property, that is a static abstract or static virtual interface member, which generated code cannot call (ADR-0070/PLAN-0071 B4/B5). No new rows, because the file is keyed by ID. - Known follow-up risk (F4, outside S5, not scheduled):ResolveSkipPropertystops at the most-derived property with the requested name, whatever its accessibility. When a derived holder declares a non-public static property that hides a public staticboolproperty of the same name on a base class, AOT reportsCMP0057. JIT's public-only reflection lookup would skip the hidden member and read the base property. That's a false compile-time rejection of usage JIT accepts, not miscompiled code. It was already present in S0 and is unverified by a runner probe. It doesn't affect interface holders (an interface'sBaseTypeis null, and JIT doesn't search base interfaces either). It needs its own decision before any change. S5 must not changeResolveSkipProperty's lookup. - Plan housekeeping: B5.9's reference to a nonexistent "Remaining risks" section now points to the Notes entries that record those risks: the 2026-11-06 B5 specification entry's decision ©, and the S4 checkpoint's known limitations. - Open: S5's corrections are pending implementation and re-review; S6–S7 are pending; B5 remains in progress. -
2026-11-09 (B5 S5 checkpoint,
compono-lsx.5): S5 (B5.8 active skip conditions and metadata) is committed at07ff636f("feat(xunitv3): support conditional AOT ComposeData skips"), whose parent is the S5 design-amendment commitf65fbf8("docs(xunitv3): clarify S5 interface skip holder compatibility"). The design owner (Claude Opus) independently re-reviewed after the F1–F3 correction round and returned final APPROVE; the product owner authorized the checkpoint commit. The branchfeat/163-composedatawas pushed toorigin(upstream established; remote HEAD07ff636f). This entry is S5's documentation closeout. - Slice state: S0–S5 completed; S6–S7 pending; B5 remains in progress.
- Scope: 21 files changed (9 modified, 12 new Verify snapshots; 2,062 insertions, 29 deletions across the slice).
src/Compono.Generators/Diagnostics/DiagnosticDescriptors.cs: added theCMP0052skip-property twinComposeDataStaticInterfaceSkipProperty.src/Compono.Generators/Discovery/AotComposeDataMethodDiscovery.cs: inserted theCMP0052check at precedence 9b in both theSkipUnlessandSkipWhenstep-9 blocks (after the 9a open-genericCMP0054check, before the 9c prohibited-propertyCMP0047checks).src/Compono.Generators/Emitters/AotComposeDataRegistrationEmitter.cs: widenedIsWithinEmissionEnvelopeto admit active attribute-levelSkipUnless/SkipWhen; addedRenderSkipCondition, which emitsstatic () => global::<HolderFqn>.<EscapedProperty>for a recorded active condition (the holder is the property's declaring type; the name is escaped).src/Compono.Generators/AnalyzerReleases.Unshipped.md: the existingCMP0052row's note extended (no new rows).test/Compono.Generators.Tests/{AotComposeDataAmendment4DiagnosticTests,AotComposeDataRegistrationEmissionTests,AotComposeDataRegistrationVerifyTests}.csandtest/Compono.XunitV3.Aot.Tests/{Compono.XunitV3.Aot.Tests.csproj,ComposeDataEmissionTests.cs}.
- S5 functionality implemented:
- Attribute-level
SkipUnless/SkipWhenrendered as static, non-capturing predicate delegates (the shared helper still owns the single metadata merge rule; the Scriban template was not changed). - Correct conditional-skip timing: predicates are bound but never evaluated during composition; only the xUnit runner evaluates them.
- Row/attribute skip-group precedence (the row group when the row's
Skipis set, else the attribute group, else none). SkipTypeand inherited holders (the emitted accessor targets the property's declaring type).- Metadata preservation (
Explicit,Label,TestDisplayName,Timeout, the traits union includingCompono.Seed,DisableParallelization). - Plain (non-virtual) static interface skip properties are supported through direct access.
CMP0052for staticvirtual/abstractinterface skip properties (instead of generatedCS8926).- No
UnsafeAccessorintroduced. - Diagnostic precedence (9a
CMP0054> 9bCMP0052> 9cCMP0047) and per-method isolation (CMP0050) preserved.
- Attribute-level
- Accepted JIT/AOT exception: an active
SkipUnless/SkipWhennaming astatic virtualinterface property is supported on JIT (condition honored) but reported asCMP0052on AOT - the explicit AOT-only parity exception recorded in ADR-0070 Amendment 4 §7a (the sixth in Amendment 4 §2's list). Astatic abstractinterface property yields the sameCMP0052but is not a parity exception, because JIT also fails it at run time (§7b). - Review corrections (all resolved before approval):
- F1: the
CMP0052ComposeDataStaticInterfaceSkipPropertydiagnostic, with the full generator diagnostic acceptance matrix, a plain-static-interface Verify snapshot, and a real-runner test. - F2:
DisableParallelizationrow override - a rowfalseoverrides an attributetrue(mergedfalse), distinguished from a Boolean OR. - F3: expanded real-runner coverage - a skip property inherited from the test class's base class, a
SkipTypeinheriting its property from a base class, and a plain[Obsolete]skip property compiled under the project'sCS0612/CS0618warnings-as-errors. - F4: the pre-existing name-hiding mismatch in
ResolveSkipProperty's lookup remains a documented follow-up risk, deferred outside S5; the lookup algorithm was not changed.
- F1: the
- Accepted nonblocking test-coverage observations (Claude Opus's final S5 APPROVE review; low severity; no further S5 changes required):
- (1) The "no registration emitted" assertion is only exercised for
static virtualinterface skip properties, notstatic abstract. Nonblocking because generation excludes any method that reports a diagnostic, and thestatic abstractCMP0052diagnostic is independently tested. - (2) The
CMP0054-over-CMP0052andCMP0052-over-CMP0047diagnostic precedence tests exercise onlySkipUnless, notSkipWhen. Nonblocking because both paths use equivalent discovery logic, andSkipWhenhas its own exact-messageCMP0052diagnostic test.
- (1) The "no registration emitted" assertion is only exercised for
- Validation (G1–G5, independently re-run by Claude Opus for approval):
- G1: full-solution
dotnet build, 0 warnings, 0 errors. - G2: full-solution
dotnet test, 5,834 total, 5,766 passed, 68 skipped, 0 failed; no pre-existing*.verified.*modified; the AOT[Compose]files untouched. - G3: generator test project 1,324/1,324 across net10.0/net11.0, with no unexpected
CMP0050; no existing verified snapshot modified. - G4: the generated registrations compile against the real
Compono,Compono.XunitV3.Aot, and xUnit AOT assemblies on net9.0, net10.0, and net11.0. - G5: the real xUnit AOT runner executes the slice's tests - 405/405 (135 per target framework).
- G1: full-solution
- Not in S5's implementation commit (by design): this PLAN-0071 checkpoint entry (uncommitted, for maintainer review). The S5 implementation commit contains the 9 modified files and 12 new snapshots only.
-
Open: S6–S7 are pending; B5 remains in progress. This S5 documentation checkpoint is uncommitted pending maintainer review.
-
2026-11-09 (B5 S6 checkpoint,
compono-lsx.5): S6 (B5.3 generic test-class bases) is committed at20c1eb9331e2c4374f7832e25ae6093949672e8d("feat(xunitv3): support generic AOT ComposeData test classes"), whose parent is the S5 documentation checkpointb1f4bb3("docs(xunitv3): record ComposeData S5 checkpoint"). The implementation was independently reviewed by Claude Opus, which returned final APPROVE with no blocking findings; the product owner authorized the S6 implementation checkpoint commit and push. The branchfeat/163-composedatawas pushed toorigin(fast-forward; remote HEAD20c1eb9). This entry is S6's documentation closeout. - Slice state: S0–S6 completed; S7 pending; B5 remains in progress. S7 remains the final Native AOT publish validation slice.
- Scope: 8 files changed (4 modified, 4 new verified snapshots; 741 insertions, 26 deletions):
src/Compono.Generators/Emitters/AotComposeDataRegistrationEmitter.cs.test/Compono.Generators.Tests/AotComposeDataRegistrationEmissionTests.cs.test/Compono.Generators.Tests/AotComposeDataRegistrationVerifyTests.cs.test/Compono.XunitV3.Aot.Tests/ComposeDataEmissionTests.cs.- Four new
AotComposeDataRegistrationVerifyTestsverified snapshots:GenericTestClass_InheritedMemberFromNonGenericRoot_RendersResolverRegistrationandGenericTestClass_MemberType_RendersResolverRegistration, each with theAotComposeDataRegistrationandRowInvokerRegistrationoutputs. No pre-existing verified snapshot was modified.
- S6 functionality implemented:
- Generic test-class emission eligibility.
- Open-generic registration indices.
- Reuse of the existing
ResolveGenericTestClassTypeclosure mechanism. - The closed constructed test class flowing into composition and diagnostics.
- Generic base inheritance.
- Multiple generic arguments.
- Nested generic test classes.
- Generic constraints.
MemberTypeand inherited non-generic member holders.- The existing unsupported-shape diagnostics.
- No new public API.
- No changes to the Scriban template.
- Validation (G1–G5):
- G1: full-solution
dotnet build, 0 warnings, 0 errors. - G2: full-solution
dotnet test, 5,882 total, 5,814 passed, 68 skipped, 0 failed. - G3: generator tests passed across net10.0/net11.0; no unexpected
CMP0050. - G4: generated-code compilation against the real
Compono,Compono.XunitV3.Aot, and xUnit AOT assemblies on net9.0, net10.0, and net11.0. - G5: the real xUnit AOT runner passed across net9.0/net10.0/net11.0; reported runner count 145 passed per target framework.
- Claude Opus independently verified G1/G2, the focused generator tests, and runner execution across all three TFMs.
- Additional independent probe (distinct from the S7 gate): Claude Opus published and executed an isolated net10.0 Native AOT probe with 0 IL/AOT warnings and 11/11 passing. This is explicitly not the S7 real-suite Native AOT publish gate (a full publish and run of the real test suite/project), which remains pending.
- G1: full-solution
- Accepted nonblocking observations (Claude Opus's final S6 APPROVE review; accepted as nonblocking, no fixes implemented):
- (A) Nested inherited theory discovery: xUnit AOT 4.0.1 does not discover theories inherited from nested base classes, including ordinary non-generic nested base classes. The nested generic theory was therefore not executed through normal xUnit discovery. Direct registered-factory tests verify its registration key and resolver. End-to-end theory execution coverage is not claimed for this nested shape.
- (B) Nested generic coverage gaps:
Outer<>.InnerBaseandOuter<>.Inner<>have index-rendering/probe evidence but no repository-level emission or real-compilation tests for those exact shapes. Claude Opus independently confirmed that both compile and register correctly.xUnit1032prevents normal theory execution for these shapes. - (C) Comment accuracy: an emitter comment groups
CMP0047with generic-shape diagnostics.CMP0047concerns obsolete/experimental usage, not generic-shape eligibility. This is a harmless documentation nit and is deferred.
- Not in S6's commit (by design): ADR, plan, research, and bead changes. This entry is S6's documentation closeout.
-
Open: S7 is pending; B5 remains in progress. This S6 documentation checkpoint is uncommitted pending maintainer review.
-
2026-11-09 (B5 S7 Native AOT infrastructure decision,
compono-lsx.5, design owner; product-owner approved): S7's validation (DeepSeek) passed G1–G5, but the required Native AOT publish oftest/Compono.XunitV3.Aot.Testsfailed for a reason unrelated to B5. Per the S7 row, the work stopped for the design owner. Claude Opus reproduced and investigated the failure in an isolatedgit archivecopy ofea94587(no repository changes), and the product owner approved the resulting design (Option D) before implementation. This entry records the decision; the S7 row in B5.11 carries the matching amendment marker. S7 remains pending until the repository implementation passes the gate below. - Blocker 1:
NETSDK1207(proven). Command:dotnet publish test/Compono.XunitV3.Aot.Tests -c Release -f net10.0 -p:RuntimeIdentifier=osx-arm64 -p:SelfContained=true -p:PublishAot=true. Error:Microsoft.NET.Sdk.FrameworkReferenceResolution.targets(121,5): error NETSDK1207: Ahead-of-time compilation is not supported for the target framework. [src/Compono.Generators/Compono.Generators.csproj].- Cause: a
-p:property is a global property. MSBuild passes it to every project in the reference graph, including during restore, soPublishAot=truereaches thenetstandard2.0generator. - The generator is reached by two analyzer
ProjectReferencepaths: (1) the test project's ownOutputItemType="Analyzer"reference toCompono.Generators.csproj; (2) transitively,Compono.XunitV3.Aot.Tests→Compono.XunitV3.Aot→Compono→Compono.csproj's analyzer-only reference toCompono.Generators.csproj(ADR-0003). This is the same propagation PLAN-0041's "AOT harness packaging note" recorded forCompono.AotSmokeTest. - Why reference metadata doesn't fix it:
GlobalPropertiesToRemove="PublishAot;RuntimeIdentifier;SelfContained"on the test project's analyzer reference still failed (path 2 remains).GlobalPropertiesToRemove="PublishAot"on both direct references (Compono.XunitV3.Aotand the analyzer) also failed, during restore, which walks the graph with the command-line globals. Fixing it at the reference level would mean changingCompono.csproj's shipped analyzer-reference shape, which PLAN-0041 already declined and this decision does not do. GlobalRuntimeIdentifier/SelfContainedare harmless; only a globalPublishAotbreaks the publish.
- Cause: a
- Blocker 2: trimmed-reflection test code (proven, surfaced once Blocker 1 was bypassed). With
PublishAotset inside the project, the publish succeeded but produced 5 IL warnings and the native run passed 143/145:ComposeDataAttributeTests.cs:77-79: 3 ×IL2075(Type.GetPropertyon aType[]loop variable).AllForms_ExposeMemberNameMemberTypeAndSeedfailed at run time (Assert.NotNull() Failure) because trimming removed the properties.PublicApiSurfaceTests.cs:17:IL2026(reported twice: compiler and trim analysis) forAssembly.GetTypes().Assembly_ExposesExactlyTheDocumentedPublicTypesfailed because trimming removed unused public types (theComposeAttribute<TProfile>andComposeAttribute<TProfile, TConfig>forms).- Both predate B5:
ComposeDataAttributeTests.cswas added in B2 (c402bc4), andPublicApiSurfaceTests.cslast changed in B4 (93c8c40). Neither warning comes fromsrc/or generated code. Every B5 test passed under Native AOT.
- Approved design (Option D, project-scoped opt-in):
- In
test/Compono.XunitV3.Aot.Tests/Compono.XunitV3.Aot.Tests.csproj:<PublishAot Condition="'$(ComponoAotTestsPublish)' == 'true'">true</PublishAot>, with a comment recording theNETSDK1207reason and pointing to this entry and PLAN-0041's note.PublishAotis then a project-local property, never a global one, so it doesn't propagate to either analyzer reference. The opt-in property itself propagates harmlessly. - The existing
ProjectReferencestructure stays as it is for normal development, G1–G5, anddotnet test. - Test adjustments (approved exactly):
ComposeDataAttributeTests.AllForms_ExposeMemberNameMemberTypeAndSeedbecomes trimming-safe without weakening its assertions: each ofComposeDataAttribute,ComposeDataAttribute<>, andComposeDataAttribute<,>is passed to a helper whoseTypeparameter is annotated[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)], which assertsMemberName,MemberType, andSeedare present. It runs in every build, JIT and Native AOT.PublicApiSurfaceTests.csis excluded from compilation only whenComponoAotTestsPublishistrue(<Compile Remove="PublicApiSurfaceTests.cs" />in a conditionedItemGroup). Enumerating all of an assembly's types is inherently incompatible with trimming. The test keeps running in every ordinary build and managed test run.- No production-code changes. No change to
src/,Compono.csproj's analyzer reference, or any package.
- In
- Internal-suite vs packaged-product validation. This opt-in proves that B5's generated code and the real working-tree
Compono/Compono.XunitV3.Aot/generator survive a real Native AOT compile and run inside the internal real-runner suite. It does not prove packaging. ADR-0041 Amendment 5's packaged-consumer policy (local-feedPackageReference, neverProjectReference, for*.AotSmokeTestandCompono.XunitV3.Aot.SampleTests) is unchanged and still governs packaged-product validation. B6's V3 (Compono.XunitV3.Aot.SampleTestsNative AOT publish) and V7 (pack-and-consume) remain B6 scope. No ADR conflict was found (ADR-0003, ADR-0041 Amendment 5, ADR-0066, and ADR-0070 checked), so no ADR amendment is made. - Scratch proof (design owner, net10.0/osx-arm64, isolated
git archivecopy ofea94587with exactly the design above): the required command exited 0 with 0IL/AOTwarnings. The published native executable ran 144/144 passed (145 minus the excludedPublicApiSurfaceTests). That includes all 101ComposeDatareal-runner tests (91ComposeDataEmissionTestsplus 10 S6 generic-class tests) and 17 helper/resolver tests (14ComposeDataRowsTests, 3ResolveGenericTestClassType*Tests). This scratch proof is design evidence only; it does not satisfy S7. -
Required S7 validation (unchanged gate, amended command):
dotnet publish test/Compono.XunitV3.Aot.Tests -c Release -f net10.0 \ -p:RuntimeIdentifier=osx-arm64 -p:SelfContained=true -p:ComponoAotTestsPublish=true \ -o <out> <out>/Compono.XunitV3.Aot.TestsPass criteria: 0
IL/AOTwarnings in the publish output; the native executable actually runs with 0 failures; the executed and passedComposeDatatests are counted and recorded. G1–G5 still apply to the implementation commit; with the opt-in unset,PublicApiSurfaceTestsmust still compile and pass. - Deferred / not required for S7: CI integration (adding a leg toaot-validation.yamlchanges the required-check design ADR-0041 Amendment 5 governs, so it needs its own decision); linux-x64 (S7 validates on the host RID only). - Open: the repository implementation of this design is pending; S7 remains pending; B5 remains in progress. This plan amendment is uncommitted pending maintainer review. - 2026-11-09 (B5 S7 checkpoint,compono-lsx.5): S7 (B5.11 completion plus Native AOT validation) is implemented and committed atfaa09a4a07f0c2fca43fe2afb99a94ca08dfef59("test(xunitv3): enable native AOT validation for ComposeData"), whose parent isa0cd87a07e4a43e41377a788f5e8c855fb2400d8. The commit is pushed toorigin/feat/163-composedata(remote HEADfaa09a4). The design owner (Claude Opus) independently reviewed the implementation and returned APPROVE; the product owner authorized the implementation commit and push. This entry is S7's documentation closeout. - Slice state: S0–S7 completed; B5 remains in progress pending final B5 review; B6–B9 untouched. S7 was the final Native AOT publish validation slice. - Implementation (exactly two files, no functional changes beyond them): -test/Compono.XunitV3.Aot.Tests/Compono.XunitV3.Aot.Tests.csproj: added the project-scoped Native AOT opt-in<PublishAot Condition="'$(ComponoAotTestsPublish)' == 'true'">true</PublishAot>, with a comment recording theNETSDK1207analyzer-ProjectReferencepropagation reason and attributing authorization to the 2026-11-09 S7 infrastructure decision (ADR-0041 Amendment 5 and PLAN-0041 establish the separate package-based consumer-validation policy; they do not authorize this internal project-scoped mechanism). The existingProjectReferencestructure is retained. Added a conditioned item group (Condition="'$(ComponoAotTestsPublish)' == 'true'") that does<Compile Remove="PublicApiSurfaceTests.cs" />, excluding theAssembly.GetTypes()-based test only from Native AOT compilation. -test/Compono.XunitV3.Aot.Tests/ComposeDataAttributeTests.cs:AllForms_ExposeMemberNameMemberTypeAndSeednow calls a helper once per attribute form; the helper'sTypeparameter is annotated[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]. All assertions and the test's intent are unchanged; it runs in every build, JIT and Native AOT, without suppressed warnings. - No production code, generator, Scriban template, public API, package-dependency, CI, ADR, or research changes. No snapshot updates; no*.received.*created; no pre-existing verified snapshot modified. - Validation evidence (G1–G5): - G1: full-solutiondotnet buildsucceeded, 0 warnings / 0 errors. - G2: full-solutiondotnet test: 5,882 total, 5,814 passed, 68 skipped, 0 failed. - G3:Compono.Generators.Tests671/671 on net10.0 and 671/671 on net11.0; no unexpectedCMP0050; no snapshot drift. - G4: real generated-code compilation intest/Compono.XunitV3.Aot.Testson net9.0, net10.0, and net11.0, 0 warnings. - G5: real xUnit AOT runnerdotnet test test/Compono.XunitV3.Aot.Tests: 145/145 on each of net9.0, net10.0, and net11.0, including managed-run coverage ofPublicApiSurfaceTests. - Native AOT publish (actual, amended command):test/Compono.XunitV3.Aot.Tests,-c Release -f net10.0 -p:RuntimeIdentifier=osx-arm64 -p:SelfContained=true -p:ComponoAotTestsPublish=true. No global-p:PublishAot=truewas used - the project-scopedComponoAotTestsPublishopt-in setPublishAotlocally. Publish succeeded with 0IL/AOTwarnings (and noNETSDK1207), producing a native Mach-O 64-bit arm64 executable. - Native execution (actual): the published binary ran with exit code 0: 144/144 passed, 0 failed, 0 skipped (TRX-confirmed). That includes all 101 discoveredComposeDatatest methods, which executed as 103 rows: the 91ComposeDataEmissionTestsmethods produced 93 rows becauseUntyped_rows_are_composedandTyped_rows_are_composedeach produce two rows, plus the 10 S6 generic-class tests. It also includes 17 helper/resolver tests (14ComposeDataRowsTests, 3ResolveGenericTestClassType*Tests) and 8ComposeDataAttributeTests. Discovered methods (101) and executed rows (103) differ only by those two double-row theories; nested-base theory rows that xUnit AOT discovery does not surface were not counted as executed. - Accepted limitations: -PublicApiSurfaceTests.csis excluded only from Native AOT compilation (enumerating all assembly types is trimming-incompatible); it remains compiled and executed in every managed build anddotnet testrun. - Nested generic base-class theory discovery remains limited by xUnit AOT 4.0.1; direct factory coverage remains valid. - This validates the working-tree generated code and the realCompono/Compono.XunitV3.Aot/generator under Native AOT; it does not validate packaged consumer behavior. - Packaged-product validation remains B6's V3 (Compono.XunitV3.Aot.SampleTestsNative AOT publish) and V7 (pack-and-consume), per ADR-0041 Amendment 5 / PLAN-0041. - CI integration is deferred - adding anaot-validation.yamlleg changes the required-check design ADR-0041 Amendment 5 governs and needs its own decision. - linux-x64 is not required for S7; S7 validates on the host RID (osx-arm64) only. - Open: B5 remains in progress pending final B5 review; B6–B9 untouched. This S7 documentation checkpoint is uncommitted pending maintainer review. -
2026-11-09 (B5 final acceptance,
compono-lsx.5): Claude Opus performed an independent, read-only final acceptance review of the three remaining general B5 criteria against the source, the generator, the template, and every generated snapshot, not against checkpoint summaries. Verdict: APPROVE. The product owner approved B5 final acceptance on that basis. B5 is accepted; all B5 acceptance checkboxes are checked. - Baseline: pre-B5
e709228("docs(xunitv3): approve ComposeData B5 AOT emission spec"), the parent of S05db86fc. Final implementation HEAD:7b311895e4836c6e18723fb3a9aac14b929e7362. - Criterion 1 (member invocation and
{M}/{T}): PASS.- The member is read through its actual declaring type (
FullyQualifiedMemberContainingTypeName, the plan'sMemberContainingTypeFqn:AotComposeDataMethodDiscovery.csrecordsselectedMember.ContainingType;AotComposeDataRegistrationEmitter.BuildMemberReadExpression). Snapshots show an inherited member read through its declaring base (ComposeDataBase.Cases,ComposeDataRoot.Cases) andMemberTypemembers through the holder. {T}istestClass.FullName + ".<Method>"in every snapshot.testClassistypeof(<Fqn>)for non-generic classes andResolveGenericTestClassType(typeof(<OpenIndex>))for generic classes, which resolves the closed runtime type.{M}istestClass.FullNamewithoutMemberTypeandtypeof(<Holder>).FullNamewith it.- No typed row element type is named: 0
TheoryData</TheoryDataRow<across all 32AotComposeDataRegistrationsnapshots. Rows are read asobject?, Task/ValueTask throughvar, and async value elements through a genericReadValueRowsAsync<TElement>. The model'sRowElementTypeNamesis not used by the emitter or template. - Supported shapes verified in snapshots: field, property, method, keyword-named member, inherited member,
MemberType, Task/ValueTask, async reference/value/mixed, and generic test classes.
- The member is read through its actual declaring type (
- Criterion 2 (reflection, LINQ, registration): PASS.
- Generated code: 0 occurrences of
System.Reflection,GetMethod/GetProperty/GetField,.Invoke(,MakeGeneric,Activator,dynamic,BaseType,GetGenericTypeDefinition,System.Linq/.Select(/.Where(/.ToArray(), orDataAttributeRegistration. All 32CreateDataRowcalls are the approved helperComposeDataRows.CreateDataRow.source.GetType()appears only as the type-name argument to theUnsupportedReturnTypefailure message (type identity, JIT message parity). - Runtime helper (
src/Shared/Compono.XunitV3/ComposeDataRows.cs): no LINQ; its only member reflection (GetProperty("DisableParallelization")) is inside#if !COMPONO_XUNITV3_AOT(the JIT package's xUnit 3.2.2 shim) and never compiles intoCompono.XunitV3.Aot. On AOT,ResolveGenericTestClassTypeis the only metadata walk (BaseType,GetGenericTypeDefinition);BindValue'stypeof(T)/Nullable.GetUnderlyingType/IsValueTypeare type identity on statically known generic arguments, part of the approved B5.5 helper contract. - Native AOT validation passed (S7).
- Generated code: 0 occurrences of
- Criterion 3 (AOT
[Compose]pipeline unchanged): PASS, byte-identical. Compared by Git blob hash (git rev-parse e709228:<path>vsgit rev-parse HEAD:<path>), not by semantic inspection:Templates/AotTheoryDataRowRegistration.scriban(528da74),Emitters/AotTheoryDataRowRegistrationEmitter.cs(3aac00f),Discovery/AotComposeMethodDiscovery.cs(6bafd59), andModels/AotComposeMethodInfo.cs(d163311): identical.- Snapshots (
git diff --name-status e709228 HEAD): B5 added 69 snapshots and modified, deleted, or renamed none. All 80 existingAotTheoryDataRow*snapshots are unchanged. - Outside those files, and not affecting the
[Compose]pipeline:ComponoIncrementalGenerator.csgained additive ComposeData wiring only;ComposedTypeAnalyzer.ContainsTypeParameterchanged fromprivatetointernalwith an identical body (reused by ComposeData discovery);GeneratorTestHelpers.csgained the additiveVerifyGeneratedSource.
- Validation: the S0–S7 evidence above stands. S7 Native AOT (net10.0/osx-arm64): 0 IL/AOT warnings; 144/144 native tests passed; 101
ComposeDatatest methods discovered and 103 rows executed. - Nonblocking observations (not fixed; candidates for B7's documentation pass):
- Stale generator wiring comment:
ComponoIncrementalGenerator.csstill describes "the emitter's own S2 feature envelope" and "its own semantics arrive in a later slice". CMP0047emitter comment (deferred since S6): anAotComposeDataRegistrationEmittercomment groupsCMP0047with the generic-shape diagnostics.- Project-file comment (S7 review): resolved at HEAD. The
Compono.XunitV3.Aot.Tests.csprojcomment now cites PLAN-0071's 2026-11-09 S7 infrastructure decision as the authority and states that ADR-0041 Amendment 5 / PLAN-0041 govern the separate package-based policy. - Unreachable member-read fallback:
BuildMemberReadExpression's?? FullyQualifiedTestClassNamecan't be reached, because emission requires a resolved member. Harmless.
- Stale generator wiring comment:
- Accepted limitations (unchanged, not reopened): the S6 observations (nested-base inherited theory discovery under xUnit AOT;
Outer<>.InnerBase/Outer<>.Inner<>probe-only coverage); the S7 scope limits (PublicApiSurfaceTestsexcluded only from the Native AOT build; packaged-product validation is B6 V3/V7; CI integration deferred; linux-x64 not required); and the S5 F4ResolveSkipPropertyfollow-up risk. - Scope: no scope creep; the only public additions are the approved hidden
ComposeDataRowshelpers (ADR-0070 Amendments 3 and 4). No CI changes. -
Open: closing the
compono-lsx.5bead is a separate step. The epiccompono-lsxstays in progress; B6–B9 remain pending. This acceptance entry is uncommitted pending maintainer review. -
2026-11-09 (B6 readiness, skip-parity investigation, and B6.0 approval,
compono-lsx.6): Claude Opus ran an independent, read-only B6 readiness review and a focused skip-parity investigation. The verdict was NEEDS DECISIONS, because the RESEARCH-0038 §2.6 item 3 check B6 requires was confirmed as a real divergence. The product owner approved the fix and the other B6 decisions. - Investigation: generated JIT and AOT probe suites (xUnit 4.0.1, net10.0) in isolated copies of
04e1a11, comparing outcome and skip reason against JIT across 70 conditional cases plus 14 under an unconditional theory skip.- HEAD: 44/70 conditional cases match JIT (14/14 unconditional). Of the 26 divergences, 20 are hard xUnit failures on valid usage (
Both 'SkipUnless' and 'SkipWhen' are set … mutually exclusive) and 6 are leaks where a row or attribute skip ran. - Root cause: xUnit AOT merges registered-factory rows with the theory field by field (
row ?? theoryforSkipUnlessandSkipWhenindependently), unlike JIT's whole-group replacement and xUnit's own AOTMemberDatapath. No runtime fix is possible (the factory gets only aDisposalTracker;TestContext.Current.TestCaseisnullat factory time). - Rejected: an always-false
SkipUnlesson every unconditional group (44/70, with 26 failures). - Approved fix (scratch-proven): theory-kind normalization: 84/84 matching JIT on the managed runner and under a real Native AOT publish (0 IL/AOT warnings); 0 changes to existing generator snapshots (671/671) with conditional emission;
Compono.XunitV3.Aot.Tests145/145 and the JIT suite with 0 failures.
- HEAD: 44/70 conditional cases match JIT (14/14 unconditional). Of the 26 divergences, 20 are hard xUnit failures on valid usage (
- Decisions: ADR-0070 Amendment 5 (approved) and the B6 decisions Q1–Q4 recorded at the top of the B6 section; B6.0 specified there as a separately reviewed slice.
- Maintainer refinements (same day): the
intcode (0/½) stays, with named internal constants in the runtime; an invalid code throws a deterministicInvalidOperationExceptionwhen normalization applies; a theory with both conditions is recorded as None, scratch-verified to leave xUnit's validation unchanged (xUnit1055at compile time on both engines; with it suppressed, 0 AOT tests generated before and after; JIT reports mutual exclusion); and the compile-time boundary is stated (AOTFactAttribute/TheoryAttributeare sealed,CS0509probed), with a B7 documentation follow-up. - B5 record: unchanged. B5's acceptance stands as a record of what B5 delivered and verified; this combination was outside B5's matrix and inside B6's.
-
Open: B6.0 implementation is pending; B6–B9 statuses unchanged. These documentation changes are uncommitted pending maintainer review.
-
2026-11-09 (B6.0 completion checkpoint,
compono-lsx.6): B6.0 (AOT skip-group normalization, ADR-0070 Amendment 5) is implemented, independently reviewed, approved, committed, and pushed at86bd7cb57a172c65fe39886f1f0db2a268934559("feat(xunitv3): normalize ComposeData theory skip parity"); its parent is the B6.0 approval documentation checkpoint281114d. The branchfeat/163-composedatawas pushed toorigin(fast-forward; remote HEAD86bd7cb). This entry is B6.0's documentation closeout. B6's remaining items (B6.1–B6.5) are outstanding; B6 overall stays in progress. - Scope: 17 files changed (+2182 −4): 9 modified, 8 new (see the B6.0 file inventory in that section above). No change to the B6 contract beyond the approved Amendment 5 shape; the implementation commit touched no ADR, plan, research, bead, or CI file.
- Implementation summary: the generator (
AotComposeDataMethodDiscovery) reads the test method's[Fact]/[Theory]namedSkipUnless/SkipWhenat compile time and records a theory skip-condition kind (None/Unless/When) onAotComposeDataAttributeMetadata; the template emitsTheorySkipCondition = <value>,only when the kind isn't None (so every existing snapshot is byte-identical); the AOT runtime (ComposeDataRows.CreateDataRow) normalizes the resolved grouped row-versus-attribute skip into the theory's own condition field after grouped precedence, using two cachedstaticAlwaysFalse/AlwaysTruedelegates (a negating closure is allocated only when converting an opposite-kind conditional predicate) and throwing a deterministicInvalidOperationExceptionfor an invalid code. The JIT path is unchanged. - Validation evidence (all green):
- G1: full-solution
dotnet build— 0 warnings, 0 errors. - G2: full-solution
dotnet test— 6,543 total, 6,048 passed, 495 skipped, 0 failed. - Generator tests: 680/680 on net10.0 and 680/680 on net11.0; no unexpected
CMP0050; no existing snapshot modified. - Managed AOT runner:
Compono.XunitV3.Aot.Testsgreen on net9.0, net10.0, and net11.0. - Native AOT (S7):
dotnet publish test/Compono.XunitV3.Aot.Tests -c Release -f net10.0 -p:RuntimeIdentifier=osx-arm64 -p:SelfContained=true -p:ComponoAotTestsPublish=true→ 0 IL/AOT warnings; the published native executable ran the matrix. - Parity matrix: 84/84 on every runner (JIT net9/10/11, managed AOT net9/10/11, Native AOT net10); 0 outcome or skip-reason mismatches (JIT vs AOT vs native).
- Snapshots: 0 existing verified snapshots modified; 4 new (both non-None kinds × the two outputs); no
*.received.*left behind.
- G1: full-solution
- Validator (
scripts/validate-theory-skip-parity.sh, added by this commit and listed in the B6.0 file inventory): a fail-closed regression guard over the 84-case matrix. It parses the MTP human-readable console output (--show-test-results all --no-ansi --progress off) — a text format, not a structured-result format; no report extension is wired, so it adds no dependency, and it is a manual verification gate, not part of CI. Supported invocation:Real-runner validation (example, JIT net10.0; the same form runs the managed-AOT and native runners):scripts/validate-theory-skip-parity.sh [--label NAME] --result FILE [--result FILE ...] scripts/validate-theory-skip-parity.sh [--label NAME] -- COMMAND [ARG ...]It rejects (nonzero exit,scripts/validate-theory-skip-parity.sh --label "JIT net10.0" -- \ dotnet test/Compono.XunitV3.Tests/bin/Debug/net10.0/Compono.XunitV3.Tests.dll \ --filter "FullyQualifiedName~ComposeDataTheorySkipMatrix" \ --show-test-results all --no-ansi --progress offRESULT: FAIL): missing cases; duplicate cases; unexpected cases; unexpected skips (expected run but skipped); unexpected executions (expected skip but passed); incorrect skip reasons; unexpected failures; input that is malformed, empty, or ANSI-contaminated with no recognizable results; a runner command that exits nonzero (including one that fails to launch); and any run-to-run mismatch across supplied runs. Exercised positively over the six managed captures and the native capture (allRESULT: PASS, all pairwiseMATCH) and negatively against empty, malformed, crashed-host, indented, ANSI, unexpected-skip, unexpected-run, wrong-reason, missing, duplicate, unexpected-case, and unexpected-failure inputs (allRESULT: FAIL, exit 1). - Independent review: Claude Opus returned APPROVE (the review-response round, which strengthened the AOT factory and JIT metadata assertions to cover all 84 cases and added the validator, was accepted with no blocking findings).
- Nonblocking observations (not fixed here): theory conditions are read only from the named arguments of
[Fact]/[Theory]themselves (both sealed on AOT,CS0509); any other test-attribute type is treated as None (B7 documents the boundary). A theory with bothSkipUnlessandSkipWhenis recorded as None by design, leaving xUnit's ownxUnit1055validation untouched. The 84-case matrix is a representative interaction matrix, not exhaustive over every condition-value or ordering combination. The validator is a manual gate over human-readable MTP output. Dogfood validation remains optional perAGENTS.md. - B5 record: unchanged. B5's acceptance stands as a record of what B5 delivered and verified.
-
Open: B6.0 is complete, reviewed, and committed; B6's remaining items (B6.1–B6.5) are outstanding; B6–B9 statuses otherwise unchanged. This B6.0 documentation checkpoint is uncommitted pending maintainer review.
-
2026-11-09 (B6.1–B6.5 slicing,
compono-lsx.6): after a fresh read-only B6 planning review by Claude Opus at4f48a16, the product owner approved slicing B6's remaining work into B6.1–B6.5, along with decisions SQ1–SQ7. Both are recorded in "B6.1–B6.5 Remaining slices" under B6. Documentation only: no code, test, snapshot, ADR, CI, or bead change. - Finding that prompted it: the plan referred to "B6.1–B6.5" without defining them. Every remaining B6 checkbox now has exactly one owner slice, except the B5.10 harness-reuse item, which is a cross-slice rule.
- Ticked: the theory-level
SkipUnless/SkipWheninteraction item, as satisfied by B6.0 (86bd7cb, 84-case matrix 84/84 on JIT, managed AOT, and Native AOT). No other item is ticked. B6.3's candidate existing coverage is listed but not credited until the audit SQ5 requires. - Review gaps the slicing addresses: JIT end-to-end R0 and seed-line checks use
Containsor regex rather than exact text (B6.1); there are no runtime C-1/Amendment 2 documenting tests (B6.1); the JIT side of Amendment 4 §1 and §2 is missing (B6.4); AOT has no real-runner fixture, execution-stage, parallel-isolation, or[InlineData]-mixing coverage (B6.2, subject to its audit); neither engine has a disposal test (B6.2); AOTSkipType, explicit, timeout, and label coverage is harness-level only (B6.3, subject to its audit). - Unchanged: B5's acceptance record; B6.0's checkpoint and evidence; the B6 decisions Q1–Q4; the S4 cancellation limitation (deferred, SQ4).
-
Open: B6.1 is next and awaits implementation authorization. B6 stays in progress; B7–B9 unchanged. This slicing checkpoint is uncommitted pending maintainer review.
-
2026-11-10 (B6.1 completion checkpoint,
compono-lsx.6): B6.1 (cross-engine text and name parity) is implemented, independently reviewed by Claude Opus (APPROVE, no blocking findings), and committed at8cd80cbef396d2364d93fb4b8bf02e24c3d62809("test(xunitv3): verify ComposeData JIT and AOT text parity"); its parent is the B6.1–B6.5 slicing checkpoint4eac0ef. The branchfeat/163-composedatawas pushed toorigin(fast-forward; remote HEAD8cd80cb). This entry is B6.1's documentation closeout. B6's remaining items (B6.2–B6.5) are outstanding; B6 overall stays in progress. - Scope: 2 files changed (+1085), both new:
test/Compono.XunitV3.Tests/ComposeDataParityTests.csandtest/Compono.XunitV3.Aot.Tests/ComposeDataParityTests.cs. Nosrc/, generator, Verify snapshot, ADR, CI, project-file, or bead change. Both files declare their parity types in the identical namespaceCompono.XunitV3.ComposedDataParity, so the expected strings are identical literals (SQ2). JIT drives the realComposeDataAttribute.GetDatapath; AOT drives the B5.10RegisteredComposeDataFactoryharness (no second harness). - Coverage (each scenario paired across the two files):
- Eight exact R0 failure scenarios: null row; unsupported row shape; too many values; null for a non-nullable parameter; type mismatch;
TConfigextraction failure; member read throwing; profile constructor throwing. Asserted with exact string equality (noContainsor regex), with the same literal in both files. - Seed-line parity: not assigned (via the null-row scenario); attribute-assigned seed; explicit attribute seed
0; profileWithSeed; negative profile seed; the duplicate-seed conflict. Mirrored fixture members and targets give the identical expected seed lines on both engines. - Literal
{M}/{T}name parity: a nested test class (the CLR+form pinned, plus the AOT registration-index.form) and a closed-genericMemberTypeholder (only the TFM-dependent assembly-qualified type-argument portion comes fromtypeof(…).FullName; the holder's namespace, name, arity, and bracket structure are asserted as literals). Names are never normalized or stripped. Verified under JIT, the managed AOT runner, and the real Native AOT binary. - C-1 inherited-member behavior: one documenting test per engine (JIT names the concrete reflected class, AOT the declaring class); deliberately not cross-asserted.
- ADR-0070 Amendment 2 documenting tests: (a) an inherited test method with a member a derived class hides, without
MemberType(JIT resolves the derived member, AOT the declaring-class member); (b) the same with an explicitMemberType(identical rows on both engines); © JIT-only, a genuinely private nestedMemberTypeholder that runs (AOT can't compile it,CMP0044). Each carries a comment naming Amendment 2.
- Eight exact R0 failure scenarios: null row; unsupported row shape; too many values; null for a non-nullable parameter; type mismatch;
- Test counts (per TFM): JIT
Compono.XunitV3.Tests+19 per TFM (net8.0/net9.0/net10.0/net11.0); managed AOTCompono.XunitV3.Aot.Tests+18 per TFM (net9.0/net10.0/net11.0) — 19 minus the JIT-only Amendment 2© test. Native AOT binary: 18 B6.1 tests executed and passed. - Validation evidence (all green):
- G1: full-solution
dotnet build— 0 warnings, 0 errors. - G2: full-solution
dotnet test— 6,673 total, 6,178 passed, 495 skipped, 0 failed. - Parity suites: JIT 19/19 and managed AOT 18/18 on net9.0, net10.0, and net11.0.
- Native AOT (S7 command):
dotnet publish test/Compono.XunitV3.Aot.Tests -c Release -f net10.0 -p:RuntimeIdentifier=osx-arm64 -p:SelfContained=true -p:ComponoAotTestsPublish=true→ 0 IL/AOT warnings; the published native executable ran 263 total, 202 passed, 61 skipped, 0 failed, including the 18 B6.1 parity tests. - B6.0 parity validator:
scripts/validate-theory-skip-parity.sh— 84/84 on JIT net10.0 and 84/84 on Native AOT, cross-run MATCH (no regression). - Snapshots: 0 verified snapshots modified;
git diff --stat -- src/empty;git diff --checkclean.
- G1: full-solution
- Independent review: Claude Opus returned APPROVE with no blocking findings (after a correction round that pinned the literal names, mirrored the seed fixtures, made the Amendment 2© holder genuinely private, removed the redundant unassigned-seed test, and asserted
InnerExceptiontypes). A review-round, test-onlyAwesomeAssertionsdependency added to the AOT test project was reverted to xUnitAssertbecauseAwesomeAssertions.dllproducedIL2104/IL3053trim/AOT warnings under Native AOT publish, which would break the 0-IL/AOT-warning gate. - Deferred to the B6.2 coverage audit: arity-1 parameterless-profile constructor failures are not covered. That path constructs the profile via
Activator.CreateInstance<T>(), which wraps any constructor exception in aTargetInvocationException, so its message may differ from the arity-2 path. B6.2 must audit the actual behavior under JIT, the managed AOT runner, and Native AOT, and, if the wrapping or text differs, stop and report the evidence before proposing any production change. Any core fix could also affect[Compose<TProfile>]and must not be introduced silently during B6.2. -
Open: B6.1 is complete, reviewed, and committed; B6's remaining items (B6.2–B6.5) are outstanding; B6–B9 statuses otherwise unchanged. This B6.1 documentation checkpoint is committed alongside the plan update.
-
2026-11-10 (B6.2 audit and streamlining,
compono-lsx.6): a read-only B6.2 coverage audit by Claude Opus at5359934, followed by a product-owner simplification review, led to ADR-0070 Amendment 6 and the "Streamlined remaining work" section under B6 (decisions D1–D7, Passes 1–4). Documentation only: no code, test, snapshot, generator, CI, project-file, or bead change. Nothing is ticked as complete by this entry. - Arity-1 constructor defect (resolves the B6.1 deferral above):
ComposeData<TProfile>(JIT and generated AOT) and everyCompose<TProfile>construct the profile through coreAddProfile<TProfile>()'snew TProfile(), which compiles toActivator.CreateInstance<T>().- Scratch probes outside the repository:
- the primitive wraps constructor exceptions in
TargetInvocationExceptionon JIT and a real Native AOT publish (net10.0, osx-arm64); - end to end through the real JIT
GetDatawith the generator, arity-1 R0 reads… failed: Exception has been thrown by the target of an invocation.withInnerException=TargetInvocationException, while arity-2 reads… failed: ctor boom; RowFailuretherefore can't carry a constructorCompositionException'sDiagnostic.- This breaks ADR-0070 Behavior item 9 and the R0 row-cause text. The product owner approved a core fix inside #163 (D1, Amendment 6), so no separate PR.
- Factual note: ADR-0018's wording that the generic form uses "no
Activator.CreateInstance" is inaccurate. Its decision and AOT-safety conclusion still hold. Amendment 6 §5 records the correction, and ADR-0018 carries a matching factual-clarification amendment (2026-11-10; the maintainer approved it after the initial planning pass). Its decision and rationale are unchanged.
-
B3-to-AOT coverage audit (SQ6). "Covered" means existing tests prove the behavior, cited rather than re-tested. Test files:
ComposeDataEmissionTests(Emission),ComposeDataParityTests(Parity),ComposeDataGetDataTests(GetData),ComposeDataTheoryTests(Theory).B3 item JIT evidence AOT evidence Action Multiple rows, distinct seed/trait, values bound, rest composed GetData GetData_NonGenericForm_ComposesEachRowAndWritesSeedTrait,GetData_DefaultSeeds_AreDistinctPerRowEmission Untyped_rows_are_composed,Partial_rows_compose_missing_parameters,TwoRows_ProduceDistinctSeedsAndFreshCompositionCovered Per-row TConfig, no cross-row leakageGetData GetData_ConfigGenericForm_ExtractsPerRowConfigAndIsolatesProfiles,GetData_DateTimeOffsetConfig_ReachesTheProfileEmission Profile_config_rows_extract_configuration, the identity shapes (Tuple_/Dynamic_/List_config_identity_shape_is_constructed),ProfileConstructorIsInvokedOncePerRowCovered (struct TConfigviaValueTuple<int,int>)[Shared](row value, composed, ordering)GetData GetData_SharedParameter_*,GetData_ComposedSharedParameter_*Emission Shared_parameter_*,Shared_explicit_value_*,SharedRows_DoNotShareInstancesAcrossRowBoundaries,Async_source_combines_with_shared_parameterCovered Nullable parameters GetData GetData_NullableParameters_AllowNullRowValuesGenerator snapshot NullableParameter_FlagsNullableDescriptor; the shared helper (ComposeDataRowsTests); ParityNull_for_non_nullable_parameter_failure_text_is_exactCovered; no new test (D2) Too many values, type mismatch, null/unsupported row, config null/wrong GetData GetData_TooManyValues_*,GetData_TypeMismatch_*,GetData_NullRow_*,GetData_UnsupportedRow_*,GetData_ConfigGenericForm_MissingConfigElement_*Emission R0 tests; Parity exact-text tests (B6.1) Covered Sync/ Task/ValueTask/IAsyncEnumerable;MemberType; inheritedGetData shape tests Emission S2/S4 success and failure tests Covered Fixtures: class, collection, inside the member Theory ComposeDataFixtureTests,ComposeDataCollectionFixtureTests,ComposeDataMemberFixtureTestsNone for ComposeData ( RequireFixtureTestscovers[Compose]only)Add (Pass 1) Parallel isolation Theory ComposeDataIsolationTests1–4None Add, 2 classes (Pass 1) Seeds and every seed-line variant; duplicate and negative seeds GetData seed tests Emission seed tests; Parity seed-line tests (B6.1) Covered Seed reproduction (pinned seed gives the same composed values) GetData GetData_PinnedSeed_ReproducesComposedValues,GetData_CapturedSeed_ReproducesComposedValuesCore determinism plus Emission AttributeSeed_IsAppliedToEveryRowCovered; no new test (D2) Metadata (skip, precedence, explicit, timeout, traits, labels, DisableParallelization)Theory real-runner classes; GetData merge tests B6.0 84-case matrix; Emission S5 tests B6.3 (Pass 1): cited, plus one AOT ExplicittheoryCollection/row shapes end to end GetData GetData_TheoryDataShape_IsSupported,GetData_SyncValueTupleRows_AreFlattened,GetData_TypedTheoryDataRowArray_IsSupported,GetData_ListOfObjectArray_IsSupportedEmission TheoryData,IEnumerable<object[]>,ImmutableArray<object[]>, async value-tuple rows; the shared helperCovered; the sync (T1,T2)[]andTheoryDataRow<T…>[]shapes aren't added (D2)Mixed [InlineData]Theory ComposeDataMixedSourceTestsNone at run time Add (Pass 1) Exclusivity GetData GetData_ComposeAndComposeData_AreMutuallyExclusiveCMP0045generator testsCovered Failure in row k (index, label, seed, InnerException)GetData GetData_FailureInLaterRow_*Emission and Parity (row 0); the shared RowFailureunit testsCovered; a later labelled-row test isn't added (D2) Member read at the execution stage Theory ComposeDataExecutionStageTestsNone Add, merged into the isolation theories (Pass 1) Existing [Compose]regression; generator discoveryFull suite Full suite; generator tests Covered Disposal None None Add: AOT harness plus a JIT documenting test (Pass 1) Assembly fixture Theory ComposeDataAssemblyFixtureTestsExcluded (xunit/xunit#3664, 4.0.1 pin) Comment only Real-token cancellation GetData GetDataCore_Cancellation*(injected token)Emission *Cancelled_PropagatesOperationCanceledDeferred (SQ4) Arity-1 profile constructor failure None None Add after the core fix (Pass 1, Amendment 6) - Streamlining (D1–D7, product owner): - B6.2 is reduced to the Pass 1 set. - B6.3 is satisfied by citation, plus at most one AOT Explicittheory and oneSkipTypetheory (SkipTypeonly if needed).- B6.4 is reduced to one JIT generic-base test. The six Amendment 4 §2 JIT documenting tests are dropped; int+stringparity is optional.- B6.5 keeps three samples; V3 and V7 move to B9. - B7 and B8 run as one pass. - B9 runs once: V2 is folded into V1, V5 and V7 share one consumer, V6 adds Compono, and V9 is a regression signal.- Four passes, each with one review and one commit that includes its plan checkpoint. - Unchanged: the B5 acceptance record; the B6.0 and B6.1 checkpoints; Q1–Q4; SQ2, SQ4, and SQ7; the single-PR policy; #161 release bundling. - Open: compono-lsx.6(B6) stays in progress. Pass 1 is next and awaits implementation authorization. B7–B9 aren't started. This ADR/PLAN update is uncommitted pending maintainer review. -
2026-11-10 (Pass 1 completion checkpoint,
compono-lsx.6): Pass 1 — the ADR-0070 Amendment 6 core fix plus the reduced B6.2/B6.3 behavioral coverage — is implemented and fully validated. Uncommitted, pending independent review and product-owner commit approval. - Core fix (Amendment 6).
CompositionBuilder.AddProfile<TProfile>()now constructs the profile through a guarded private staticCreateProfile<TProfile>()(ExceptionDispatchInfo.Capture(inner).Throw()on aTargetInvocationExceptionwhoseInnerExceptionis non-null);ApplyProfile,AddProfile(ICompositionProfile), and cycle detection are unchanged. The onlysrc/change issrc/Compono/CompositionBuilder.cs(+35/-2, including the XML docs). No generator, emitter, template, diagnostic, snapshot, or API-reference change. - Tests added. Core
test/Compono.Tests/ComposerProfileTests.cs(three regression tests: same-instance + stack frame;CompositionException/Diagnosticby reference;Configure-thrownTargetInvocationExceptionnot unwrapped). Arity-1 ComposeData parity in bothComposeDataParityTests.cs(JIT through the realGetData; AOT through the harness). Newtest/Compono.XunitV3.Aot.Tests/ComposeDataTests.cs(B6.2: Class AIClassFixture<RequireFixtureIdFixture>+ Class B[Collection]/ICollectionFixture<RequireFixtureIdFixture>, each reading the fixture in itsRows()member and asserting the execution stage and isolation; mixed[InlineData]+[ComposeData]; disposal ownership boundary; assembly-fixture comment; and the B6.3Explicittheory). JIT disposal documenting test added totest/Compono.XunitV3.Tests/ComposeDataGetDataTests.cs. One harness change: an optionalDisposalTracker? disposalTracker = nullparameter onRegisteredComposeDataFactory.InvokeAsync. - B6.3 evidence. Ticked by citation (see the B3-to-AOT audit table above). Skip conditions/precedence/theory-level: the B6.0 84-case matrix, the AOT S5 harness tests, and the JIT baseline.
Explicit: addedComposeDataExplicitTests(native run reports its two cases as skipped; theAssert.Failbody never runs). Timeout/traits/labels andDisableParallelization: cited, no new tests. NoSkipTypereal-runner theory was added. - Validation. G1 full build 0 warnings/0 errors; G2 full
dotnet test6,733 total, 6,232 passed, 501 skipped, 0 failed; no*.verified.*changed. S7 Native AOT publish (net10.0,osx-arm64,-p:ComponoAotTestsPublish=true) reported 0 IL/AOT warnings; the native executable ran 275 total, 212 passed, 63 skipped, 0 failed (275 = the managed 276 minus thePublicApiSurfaceTestsexcluded under publish). The new arity-1 parity (Profile_parameterless_constructor2/2), Class A (5/5), Class B (2/2), Explicit (2 skipped), and disposal (1/1) tests all execute in the native binary. B6.0 validator 84/84 PASS on JIT net10.0 and Native AOT.git diff --stat -- src/lists onlysrc/Compono/CompositionBuilder.cs.git diff --checkclean. - Deviations. The JIT disposal target is a private
[ComposeData<TrackedDisposableProfile>(nameof(DisposalRows))]dispatch-only method (no[Theory]), withComposeDataTrackedDisposableandTrackedDisposableProfiledeclared at namespace scope, because the generator refuses row-binding dispatch for private nested parameter types (CMP0013); the attribute is wrapped in#pragma warning disable xUnit1008/restoresince it is a generator-dispatch request, not a test-data source. -
Open: Pass 1 is implemented and validated, uncommitted pending review. Pass 2 (reduced B6.4 + B6.5 samples) isn't started. The cross-pass harness rule stays open until Pass 2 closes.
-
2026-11-10 (Pass 2 completion checkpoint,
compono-lsx.6): Pass 2 — the reduced B6.4 JIT generic-base regression plus the B6.5 samples — is implemented and fully validated. Uncommitted, pending independent review and product-owner commit approval. Nosrc/change: the generic-base test found no production defect, and the source generator was not modified. - B6.4 (JIT generic bases, ADR-0070 Amendment 4 §1). Appended to
test/Compono.XunitV3.Tests/ComposeDataTheoryTests.cs(real runner, AwesomeAssertions): a semantic probe (ComposeDataGenericDeclaringTypeProbeProvider/…Profile/record ComposeDataGenericDeclaringTypeProbemirroring the AOTDeclaringTypeProbe*types), a non-generic row holder (ComposeDataGenericMemberHolder), a non-generic root (ComposeDataGenericRoot), an abstractComposeDataGenericBase<T> : ComposeDataGenericRootdeclaring two inherited[Theory][ComposeData<…ProbeProfile>]methods (one viaMemberType = typeof(ComposeDataGenericMemberHolder), one via the inherited root member), and two closed subclassesComposeDataGenericIntTests : ComposeDataGenericBase<int>andComposeDataGenericStringTests : ComposeDataGenericBase<string>. Each case asserts the row label andprobe.DeclaringType.Should().Be(GetType().BaseType)— the same closed-declaring-type assertion as the AOT S6 tests. The four cases pass; JITtestMethod.DeclaringTypeis already the closed constructed base (the reflectionDeclaringTypeof an inherited method reflected from a closed generic subclass), so nosrc/normalization was needed. The six Amendment 4 §2 documenting tests were not added, and no R0 parity test was mirrored (B6.4 scope). - B6.5 (samples). Two new documentation-quality files, the same three examples in each:
test/Compono.XunitV3.SampleTests/ComposeDataTests.cs(packaged JIT consumer; AwesomeAssertions, global usings) andtest/Compono.XunitV3.Aot.SampleTests/ComposeDataTests.cs(packaged AOT consumer; plainAssert, explicitusing Compono.XunitV3.Aot; using Xunit;). Example 1 — per-row configuration:[ComposeData<PricingProfile, PricingConfig>]reads a distinctPricingConfigfrom element 0 of each row (config first, then the row'sprice/expectedbound positionally), with a composedPriceCalculatorbuilt from that row's config. Example 2 — a shared dependency: a row-suppliedCatalogRepositorymarked[Shared]is reused by a composedCatalogService(Assert.Same/BeSameAs), proving the shared instance flows into composed dependents. Example 3 — a fixture-backed row: a class fixture (CatalogDatabaseFixture : IAsyncLifetime) is read from the row member (TestContext.Current.RequireFixture<…>()) and again from a profile (CatalogConnectionProfile), asserting both equal the fixture's initialized value — the execution-time read is valid becauseComposeData'sSupportsDiscoveryEnumeration()isfalse. No async source and no redundant variants were added. - Files changed. Modified:
test/Compono.XunitV3.Tests/ComposeDataTheoryTests.cs(+56). New:test/Compono.XunitV3.SampleTests/ComposeDataTests.cs,test/Compono.XunitV3.Aot.SampleTests/ComposeDataTests.cs. Nosrc/change (git diff --stat -- src/empty); no*.verified.*change. - Validation. G1 full
dotnet build0 warnings / 0 errors. G2 fulldotnet test6,749 total, 6,248 passed, 501 skipped, 0 failed (up from Pass 1's 6,733 total by the four new JIT generic-base cases). Focused JIT generic-base run (net10.0): 4/4. Affected sample projects: packaged JIT sample (net10.0,--filter-not-class "Compono.XunitV3.SampleTests.Failing*") 17/17; packaged AOT sample under the managed runner (net10.0) 10/10. Native AOT:dotnet publish test/Compono.XunitV3.Aot.SampleTests -c Release -f net10.0 -p:RuntimeIdentifier=osx-arm64 -p:SelfContained=true -p:PublishAot=true -p:UseAppHost=truereported 0 IL/AOT warnings, and the native executable ran 10/10, 0 failed. No generator snapshot churn. Only the three intended paths appear ingit status. - Deviations. None of substance. The optional
int+stringcross-engine parity test was not added (no gap shown). The AOT sample reuses the existing class-fixture pattern (IClassFixture<…>), consistent with the xunit/xunit#3664 constraint documented in the file. Native AOT publish/run ofCompono.XunitV3.Aot.SampleTestswas run here additionally, ahead of the Pass 4 V3 requirement, because the AOT sample changed; it will run once more on the final tree in Pass 4 V3. -
Open: Pass 2 is implemented and validated, uncommitted pending independent review. Pass 3 (B7 + B8 as one pass) is next.
compono-lsx.6(B6) stays in progress until Pass 2 is committed; B7–B9 aren't started. The cross-pass harness rule is confirmed closed (Pass 2 added no harness change). -
2026-11-10 (Pass 3 completion checkpoint,
compono-lsx.7+compono-lsx.8): Pass 3 — B7 (documentation and API reference) and B8 (distributedcomponoskill) as one pass — is implemented and validated. Uncommitted, pending one independent Claude Opus review and product-owner commit approval. Documentation and developer-experience only: no production, generator/emitter/template, diagnostic, snapshot, CI, or project-file change; nosrc/change (git diff --stat -- src/empty). - B7 consumer docs.
docs/packages/compono-xunitv3.mdgained a full "Composed theory rows ([ComposeData])" section (the three forms; row source and member shapes; row layout and strict leading-positional binding incl. nullable/value-type rules; per-row configuration;[Shared]; reading xUnit fixtures from the row member; execution-time timing; skip/label/trait/DisableParallelizationmetadata merge; disposal; failures/seeds/row labels; mixing with xUnit[InlineData]/[MemberData]/[ClassData]; and a Native AOT subsection). The "No stacking…" bullet in "What it deliberately doesn't do" was rewritten to a pointer to that section (exclusivity stated, multi-row answer = one[ComposeData]).docs/packages/compono-xunitv3-aot.mdgained the matching "Composed theory rows ([ComposeData])" + "ComposeData vs.Compono.XunitV3: where they differ" section, a "ComposeData diagnostics on Native AOT" section (theCMP0051–CMP0057table plus the ComposeData uses ofCMP0040/CMP0042/CMP0044–CMP0049, the AOT-only exclusions, and generic test-class bases being supported), and the comparison table's[ComposeData]row; the stale pre-B4/B5 "Not yet supported"[ComposeData]paragraph was rewritten to a pointer. Newdocs/cookbook/vary-composed-configuration-per-theory-row.mdand its alphabeticaldocs/cookbook/index.mdentry.docs/troubleshooting/common-errors.mdgained the[ComposeData]D1–D5/R0 symptom section (member-level messages, the R0 row wrapper, seed-line rules, and the Amendment 6 profile-construction note).docs/reference/diagnostics.mdgained theCMP0051–CMP0057entries plus the intro/attribution updates.docs/migrating-from-autofixture.mdgained the[InlineAutoData]/[MemberAutoData]→[ComposeData]concept-map rows, a reworked "one attribute" paragraph, and a new "Migrate a multi-row[InlineAutoData]/[MemberAutoData]table" section.docs/how-to/use-profiles.mdgained the Amendment 6 profile-construction-failure sentence.docs/adr/0022-compono-xunit-package-design.mdgained Amendment 8 (the multi-row case Amendment 7 left open is answered by ADR-0070's[ComposeData], no change to[Compose]). - B7 V4 (API reference). Regenerated
docs/reference/apiwith the established workflow (build each publishable package Releasenet10.0, then.github/scripts/generate-api-reference.sh). Result: exactly 4 modified files —docs/reference/api/Compono/Compono.CompositionBuilder.md,docs/reference/api/Compono/Compono.CompositionBuilder.AddProfile.md(Pass 1'sAddProfileXML-doc correction, incl. the constructor-exception Remarks),docs/reference/api/Compono.XunitV3/Compono.XunitV3.md, anddocs/reference/api/Compono.XunitV3.Aot/Compono.XunitV3.Aot.md— plus newComposeDataAttribute*pages under both packages, the AOT-only[ComposeData]infrastructure pages (ComposeDataRows,ComposeDataAttributeMetadata,RowContext,RowData; public +EditorBrowsable(Never)), and the consumer-facingComposeData-familySharedAttribute(ADR-0070 Amendment 3's marker — not hidden infrastructure). Deterministic: the whole-tree sha ofdocs/reference/api(fc3faaa624301b921fc0dbaa79bbaf34d40cbf15) is byte-identical on a second run. No unrelated churn. - B8 skill.
skills/compono/references/xunit-v3.mdgained the "[ComposeData]— composed theory rows from a member" section and its "Hard constraint: one Compose-family attribute per method" section was reworked (exclusivity still applies; the multi-row answer is one[ComposeData]).skills/compono/SKILL.mdgained the Compono.XunitV3/XunitV3.Aot detection rows, a Decide-step[ComposeData]bullet, a no-stacking Guardrail bullet, and updated frontmatter (description now lists[ComposeData]).references/patterns-and-antipatterns.md(antipattern #9 reworked, new #18 one-method-per-row, mapping rows),references/diagnostics.md(stale "complete core" paragraph fixed; newCMP0040–CMP0049/CMP0051–CMP0057table;[ComposeData]row-failure runtime note), andreferences/registrations-profiles-and-scopes.md(per-row profile withTConfig) were updated.skills/compono/evals/evals.jsongrew from 57 to 60 evals (ids 58–60: JIT per-row configuration; Native AOT row binding; fixture-backed theory data); file remains exactlyjson.dumps(d, indent=2) + "\n"and byte-identical on roundtrip. - Validation. G1 full
dotnet build(whole solution, every TFM) 0 warnings / 0 errors.mkdocs build --strict(uv run mkdocs build --clean --strict) passes, 0 warnings. V8: skill-creatorquick_validateonskills/componoreturns "Skill is valid!"; all](…)targets underskills/compono/**resolve (0 bad);evals.jsonparses, is schema-clean, has no duplicate ids (min 1, max 60). No*.verified.*change; nosrc/change;git statusshows onlydocs/**andskills/compono/**. - Deviations. (1) The V8 validator rejects
</>in the frontmatter description; the pre-existing description already containedcomposer.Create<T>()/Register<T>()(so it failedquick_validateat HEAD too). Fixed minimally by de-angle-bracketing those tokens (Create/CreateMany,Register/.For) — description stays ≤ 1024 chars. (2) Onemkdocs --strictanchor mismatch (a hyphenated fragment for the new multi-row migration heading) was fixed. (3) The B7 touched-doc set excludesdocs/troubleshooting/faq.mdanddocs/how-to/write-a-composed-theory.md; both mention the Compose-family exclusivity rule but state nothing false (they are notComposeDatapages), so they were left unchanged (flagged for a possible follow-up, not a Pass 3 obligation). (4) The B8 "compile-check the snippets" item is satisfied by construction: the snippets mirror the compiled Pass 2 sample tests and were checked against the regenerated API reference, but were not separately recompiled. (5) The plan's third mandated eval scenario ("doesn't invent[InlineData]+[Compose]merging") was replaced by the fixture-backed scenario (eval 60); the no-merge case is only partly covered by eval 58's expectation 4. (6) This checkpoint's V4 bullet initially mislabelledSharedAttributeasEditorBrowsable(Never)infrastructure — it is a consumer-facing marker; corrected above. - Open: Pass 3 is implemented and validated, uncommitted pending independent review. Pass 4 (B9 final validation: V1
dotnet build+dotnet test, V3 Native AOT, the combined V5/V7 consumer, V6 package validation/ApiCompat, V9 spot-check) is next.compono-lsx.7andcompono-lsx.8are implemented pending review;compono-lsx.9(B9) is not started. - 2026-11-10 (Pass 3 review-response,
compono-lsx.7+compono-lsx.8): an independent review requested changes on six documentation/skill correctness defects; all fixed (docs and skill only — no production, generator/emitter/template, diagnostic, snapshot, or project-file change; still uncommitted). - (B1) Non-public row members in examples made
public static(docs/migrating-from-autofixture.md,skills/compono/references/xunit-v3.md). - (B2) The
docs/reference/diagnostics.mdCMP0051–CMP0057message text was replaced with the verbatim descriptors fromDiagnosticDescriptors.cs(ComposeData member '{0}' for '{1}' …, where{1}is the method display name, not the test class). - (B3) The AOT-only exception set was corrected everywhere: the AOT package guide now lists all eight AOT-only exclusions (ADR-0070 Amendments 2 + 4 §2a–e + §7a) with workarounds, marks
static abstractas not an exception (§7b, JIT fails it too), and adds the §2e supported mixedIAsyncEnumerableshape and the C-4 accepted residual; everyCMP0051–CMP0057footer and the skill now say the JIT engine reports the equivalent condition at run time except the AOT-only Amendment 2/4 cases, which JIT accepts (CMP0057on JIT is xUnit'sTestPipelineException); the "assembly fixtures" item was relabelled as xUnit's own limitation, not a Compono diagnostic; eval 59's expectation 3 was rephrased. - (B4) The seed-line rule was corrected everywhere: an
R0failure before the row'sCreateRow(row shape,TConfigselection, profile construction,Configure, the composer build) prints the attribute seed or "not assigned", while a failure fromCreateRowon — including row-value validation and too many values — printsSeed: {row.Seed}. - (B5) Disposal guidance inverted to the real hazard (JIT disposes every
IDisposabletheory argument, a[Shared]parameter included). - (B6) ADR-0022 Amendment 8's date corrected to 2026-11-10 (its writing date), and the
[MemberAutoData]"stacked" wording narrowed to[InlineAutoData]. - Non-blocking items also fixed: the cookbook's "attribute-legal" wording, the
[MemberData]one-row comparison, the member-vs-row-index claim, the nested-test-class note, theCMP0046–CMP0049table split (arity-2TProfile<TConfig>vs arity-1 parameterless), and the skill's stale "profile reused across rows" reason (now: fresh profile instance per row). -
Re-validation after the fixes:
dotnet build0 warnings / 0 errors;mkdocs build --clean --strictpasses with 0 warnings; skill-creatorquick_validatereturns "Skill is valid!";evals.jsonstill 60 entries and byte-identical on roundtrip; nosrc/or*.verified.*change. -
2026-11-10 (Pass 4 / B9 completion checkpoint,
compono-lsx.9): B9 final validation and release-readiness gate, run once on the final tree. All gates passed. This checkpoint changes only this plan (docs/plans/0071-…) — no production, generator/emitter/template, diagnostic, snapshot, CI, project-file, sample, or test change (git diff --stat -- src/ test/ .github/empty). - V1 full build + test. Debug
dotnet build0 warnings / 0 errors. Solutiondotnet test(every TFM) 6,749 total, 6,248 passed, 501 skipped, 0 failed. - Snapshot verification. No
*.received.*produced;git statusclean; no*.verified.*change. - V3 Native AOT (osx-arm64, net10.0).
test/Compono.XunitV3.Aot.Tests(S7 publish): 0 IL/AOT warnings; ran 275 total, 212 passed, 63 skipped, 0 failed (incl. the Amendment 6 arity-1 parity cases).test/Compono.XunitV3.Aot.SampleTests: 0 IL/AOT warnings; ran 10/10, 0 failed, with all three[ComposeData]samples among the executed tests. - Linux-x64 Native AOT CI config. Reviewed, not locally executed (host is darwin/arm64). The
aot-xunitv3-aot-pipelinejob publishes and runsCompono.XunitV3.Aot.SampleTestsonubuntu-latest(required MSBuild-prop publish form), gated byxunitv3_aot_applicable. That flag is true only for changes undersrc/Compono.XunitV3.Aot/**,src/Shared/Compono.XunitV3/**, or the two AOT test projects; this docs/skill/plan-only PR matches none, so the leg skips (the fail-closedaot-gatepasses a skipped-but-not-applicable leg). No workflow change. Linux execution is left to CI. - V5 + V7 packaged consumer. One scratch consumer outside the repo, packages packed from the final tree at
1.4.0-preview.9999.4and consumed as local-feedPackageReferences. JITxunit.v3.mtp-v23.2.2 → 9/9; JIT 4.0.2 → 9/9; AOTxunit.v3.aot.mtp-v24.0.2 → published with 0 IL/AOT warnings and ran 8/8. Exercised all three forms,[Shared], an async source, a fixture-backed row, a[Compose]regression, and the arity-1 Amendment 6 ctor-failure.obj/project.assets.jsonconfirms all three packages resolved to the local-feed1.4.0-preview.9999.4(no stale cache / repo-output resolution). - V6 package validation + ApiCompat.
Compono,Compono.XunitV3,Compono.XunitV3.Aotpacked against baseline1.4.0-preview.122; ApiCompat reported no breaking changes for all three (Compono behavior-only; both xUnit packages additive-only);inspect-packed-nupkgs.shconfirms expected contents/metadata and no unexpected warnings. - V9 per-row cost spot-check. Release
net10.0, 3 warmups / 50 iterations:[ComposeData]50 rows = 9.061 ms (0.0036 ms/row) vs 50[Compose]methods = 4.434 ms (0.0018 ms/method), ratio 2.043× — at the documented 2× investigation trigger, consistent with the pre-existing figure and explained byComposeDatadoing strictly more per row (member read + row build + value binding + freshComposer), so a regression signal, not a new regression. No speculative optimization. - Docs / skill / API gates.
uv run mkdocs build --clean --strictpasses, 0 warnings; skill-creatorquick_validate skills/compono→ "Skill is valid!";evals.jsonparses (60 entries, ids 1–60, byte-identical roundtrip);git diff --exit-code -- docs/reference/apiclean, no drift (the Pass 3 regeneration is stable). - Release-readiness verdict. READY (pending product-owner decision to open the PR). B1–B8 are complete and reviewed; JIT/AOT parity holds within the approved exceptions (the eight AOT-only rejections of ADR-0070 Amendments 2/4, generic test-class bases supported, the C-4 residual documented); the public API is documented; samples compile and execute on both engines; diagnostics (
CMP0040–CMP0049,CMP0051–CMP0057) and the skill/evals are documented/valid; and packaged-consumer + package-validation compatibility is proven. - Recorded limitations (not blockers). Linux-x64 AOT execution is CI's to prove and skips on this docs-only PR; the AOT plain
[Compose]inline-values/[Shared]form remains the separate follow-up already tracked in ADR-0070/its Amendments (not part of[ComposeData]parity); the ~2.04× per-row cost is an investigation trigger only; external dogfood was not run (optional per policy). - Open: B9 is complete and validated. The single cohesive Pass 1–4 feature is on
feat/163-composedata, committed (Pass 3 docs/skill/app), with this Pass 4 plan checkpoint committed alongside it. PR creation, Beads closure, and merge await product-owner action.