[PLAN-0055] Compono.Logging: First-Class Microsoft.Extensions.Logging Testing Support — Implementation Plan¶
Status: Done
Implements: ADR-0055 (including Amendment 1 — generated closed-generic activation for ILogger<T>; Amendment 2 — public LoggingFactoryRegistry; Amendment 3 — logging activation generation lives in the existing Compono.Generators, gated by ComponoGeneratedLogging)
Goal¶
A new Compono.Logging package ships with UseLogging(), a CapturingLogger/CapturingLogger<T> pair, CapturedLogEntry, direct inspection (GetCapturedEntries()/GetLastCapturedEntry()/ ClearCapturedEntries()), and Verify()-based fluent verification — exactly the public API ADR-0055 froze, including its amended contracts (non-Compono-logger failure semantics, real MinimumLevel filtering with correct LogLevel.None handling, direct-construction support, Amendment 1's generated closed-generic activation for ILogger<T>, and Amendment 3's ComponoGeneratedLogging-gated generation living inside the existing Compono.Generators). Done means: Compono.Logging.nupkg (runtime assembly + its tiny MSBuild props asset — no generator/analyzer DLL of its own) and Compono.nupkg (its existing Compono.Generators.dll, now also emitting logging activation) both build and pack, every ADR-0055 behavioral contract — including Amendments ⅓'s activation/discovery/property-gating/diagnostic contract — has a passing test proving it, Compono.Logging is AOT/trim-clean end-to-end through the generated composition path (not only direct construction), every documentation/skill surface ADR-0055 names is updated and consistent with the shipped API, the compono skill's eval suite has a passing Compono.Logging scenario exercising real UseLogging() composition, and both dogfood targets (LayeredCraft.StructuredLogging, alexa-vox-craft) have been validated against freshly packed local packages via scripts/dogfood-validate.sh — see "Status contract" below for exactly how that last item relates to this plan's own Status: Done.
Scope¶
In scope: everything ADR-0055's Decision Outcome (including Amendments 1-3) specifies — Compono.Logging package creation, logging activation generation added to the existing Compono.Generators (reusing its discovery pipeline directly, gated by ComponoGeneratedLogging), LoggingFactoryRegistry, LoggingProvider/ stage-6 registration, the full public API surface, structured-state/ scope/concurrency/MinimumLevel semantics, direct construction, the non-Compono-logger failure diagnostic, the missing-generated-activation diagnostic, the missing-runtime-symbols compile-time diagnostic, and the full documentation/skill/eval synchronization ADR-0055 makes part of completion.
Explicitly deferred (per ADR-0055's own "Explicit initial-package boundaries" and [Shared] sections, and per Amendments ⅓'s own scope limits — not this plan's job to solve): ILoggerFactory, Serilog/provider-specific behavior, test-runner output capture/routing, DI integration, cross-scope structured-property flattening, FakeLogger-style per-level ControlLevel, a category-string CapturingLogger constructor, any change to [Shared] or a new sharing mechanism, any change to the composition pipeline/provider precedence model, any change to TransitiveClosureWalker/LeafTypeClassifier's visibility, solving ADR-0052's still-open "Finding B" (nested context.Resolve<T>() discovery), and changing Compono.TestDoubles' own ComponoGeneratedTestDoubles default (see "Not in this plan: Compono.TestDoubles' default," below).
PR sequencing: task groups 1-17 — the Compono.Logging runtime package, the logging-activation addition inside Compono.Generators, LoggingFactoryRegistry, every behavioral test group, Native AOT/trimming validation, and the full documentation/skill/eval synchronization — are all in-repo work and ship as one coherent, single PR. ADR-0055 (with its amendments) still describes one package, one decision; the generator addition is part of that one decision, not a separately-scoped feature, so it does not get its own PR. Task group 18 (dogfood migration against LayeredCraft.StructuredLogging/ alexa-vox-craft) touches separate repositories and, per design-decisions.md's phase-per-PR rule, ships as its own follow-on PR(s) against those repos after Compono.Logging itself has merged and been published — the same sequencing PLAN-0051 used for its own alexa-vox-craft migration (task group 10 there). See "Status contract" below for how task group 18 relates to this plan reaching Status: Done.
Status contract: PLAN-0055 follows PLAN-0051's real, inspected precedent — its own Status: Done requires task group 18 (dogfood migration) to be substantially complete, exactly like task groups 1-17, even though task group 18's actual code changes land as separate PRs in layered-craft/structured-logging/alexa-vox-craft, not in this repo (PLAN-0051 itself reached Status: Done only after its own alexa-vox-craft dogfood migration, task group 10, was substantially complete, despite that migration's commits landing in a separate repo).
Not in this plan: Compono.TestDoubles' default. ADR-0055 Amendment 3 explicitly declines to change ComponoGeneratedTestDoubles' existing pure-opt-in behavior here — a real, confirmed compatibility risk (any existing consumer who references Compono.TestDoubles, calls both UseGeneratedTestDoubles() and UseNSubstitute()/another stage-6 provider, and never set the flag today is currently, silently, served entirely by the other provider; flipping the default would silently change that) rules out treating it as a mechanical follow-on. Tracked as a separate, future ADR-0043 amendment plus its own small implementation plan — not part of PLAN-0055.
Tasks¶
1. Package/project creation (Compono.Logging runtime)¶
- Create
src/Compono.Logging/Compono.Logging.csproj—TargetFrameworksmatching the rest of the repo (net8.0;net9.0;net10.0;net11.0, per ADR-0038),LangVersion latest,ImplicitUsings enable,Nullable enable. -
<Title>/<Description>followingCompono.Http.csproj's precedent (a real, specific description, not boilerplate). -
<IsAotCompatible>true</IsAotCompatible>— per ADR-0055's AOT/trimming section (as corrected by Amendment 1), followingCompono.Http's precedent exactly. -
ProjectReferenceto..\Compono\Compono.csprojonly, plusPackageReferencetoMicrosoft.Extensions.Logging.Abstractions(the only new external runtime dependency ADR-0055 names) — noMicrosoft.Extensions.Logging, noMicrosoft.Extensions.DependencyInjection, noMicrosoft.Extensions.Diagnostics.Testing, noCompono.TestDoubles, noCompono.NSubstitute. - No
ProjectReference/OutputItemType="Analyzer"of any kind — per Amendment 3,Compono.Loggingowns no generator project at all. Logging activation generation is emitted by the existingCompono.Generators.dll, already packed insideCompono.nupkg, which everyCompono.Loggingconsumer already receives transitively through its mandatoryComponodependency (confirm this transitivity directly during task 17, don't just assume it). - Pack the new
ComponoGeneratedLoggingdefault-value MSBuild props asset (task 17's own subtask covers authoring it) into bothbuild/Compono.Logging.propsandbuildTransitive/Compono.Logging.propspackage paths, mirroring exactly howCompono.csprojpacksbuild/Compono.propstoday (two<None Include=... Pack="true" PackagePath="build\Compono.Logging.props" />/PackagePath="buildTransitive\Compono.Logging.props"entries pointing at the same source file). - Copy the
PinProjectReferenceVersionsExactMSBuild target fromCompono.Http.csproj(ADR-0031/PLAN-0008 Phase 0 pattern). -
InternalsVisibleToforCompono.Logging.Tests. - Add
src/Compono.Logging/Compono.Logging.csprojto the solution file(s) alongside the othersrc/Compono.*projects. - Create
test/Compono.Logging.Tests(mirrorstest/Compono.Http.Tests's project shape) andtest/Compono.Logging.AotSmokeTest(mirrorstest/Compono.Http.AotSmokeTest's two-proof pattern — see task 15).
2. Logging activation generation inside the existing Compono.Generators (ADR-0055 Amendment 3)¶
No Compono.Logging.Generators project exists. This task adds a narrowly scoped, clearly separated feature to the existing src/Compono.Generators assembly, reusing its discovery pipeline directly rather than reimplementing it.
- Well-known-type resolution: extend
Compono.Generators' existingWellKnownTypesinfrastructure (the same pattern already used forSystem.DateTime/etc.) with optional, nullable entries forMicrosoft.Extensions.Logging.ILoggerand the open genericMicrosoft.Extensions.Logging.ILogger1, resolved viaCompilation.GetTypeByMetadataName— both may legitimately benullfor a consumer who never referencesMicrosoft.Extensions.Logging.Abstractions. **Compono.Generators.csprojgains noMicrosoft.Extensions.Logging.Abstractionspackage reference** — verify this compiles and behaves correctly (returnsnull` cleanly) against a compilation that never references that package at all. -
ComponoGeneratedLoggingcompiler-visible property: read once viaAnalyzerConfigOptionsProvider, the same shapeComponoGeneratedTestDoublesalready uses (ComponoIncrementalGenerator.Initialize's existingtestDoublesEnabledprovider is the direct template to copy). This property is the sole enable/disable switch for this feature — see the property-declaration/default-value tasks under task 17. Package/ type presence is deliberately not the gate — do not useLoggingFactoryRegistry's resolvability (or any otherCompono.Loggingsymbol) as the enabling signal; check that only after the property already says "enabled." - Discovery reuses the existing pipeline, not a reimplementation of it. Extend
LeafTypeClassifier/TransitiveClosureWalker's existing per-parameter-type classification (the exact call siteIsGeneratedTestDoubleEligiblealready uses,src/Compono.Generators/Discovery/TransitiveClosureWalker.cs:189) with one more, independently-optional check: is this parameter type a closed instantiation ofILogger<T>? WhenComponoGeneratedLoggingis enabled and the type matches, recordTinto a new discovery bucket — do not add a second walker, do not duplicate root discovery or leaf classification. This reuses, unmodified, the same rootsTransitiveClosureWalkeralready covers (Composer.Create<T>()/CreateMany<T>(),[Composable]in both forms,[Compose]/[Compose<TProfile>]theory-row parameters) and the same constructor-selection outcome every other discovery path already relies on. - Deduplicate: recording the same closed
Tmore than once (reached via two different roots, or the same root twice, possibly across different syntax trees) emits exactly one registration for it. - Required-runtime-symbols validation, only once the feature is enabled: when
ComponoGeneratedLoggingis enabled and at least one closedILogger<T>category was discovered (or, per the diagnostic matrix below, even if none was — the property being explicitlytruewith aCompono.Loggingreference absent is itself the failure case), resolveCompono.Logging.LoggingFactoryRegistry,Compono.Logging.CapturingLogger1, andCompono.Logging.LoggingOptionsviaGetTypeByMetadataName`. All three resolvable → proceed to emission. Any missing → report the diagnostic below and emit no logging-registration source at all (never partial/broken generated code referencing types that don't exist). - New diagnostic (working id
CMP0038— confirm the actual next-available id againstsrc/Compono.Generators/AnalyzerReleases.Unshipped.mdat implementation time, since other in-flight work may have already claimed a number; the working id here is a placeholder, not frozen), categoryCompono.Logging,Infoseverity (matchingCompono.TestDoubles' ownCMP0020-CMP0032informational convention — nothing broken is emitted, so this isn't a build error): "ComponoGeneratedLoggingis enabled butCompono.Logging's runtime types could not be resolved — isCompono.Loggingreferenced?" Add itsAnalyzerReleases.Unshipped.mdrow alongside the existingCMP0020-CMP0037entries. - Emitter, clearly separated: a new emitter file (e.g.
Emitters/LoggingActivationEmitter.cs) plus its own template (e.g.Templates/LoggingActivation.scriban), mirroring howTestDoubleAnalyzer/Templates/TestDouble.scribanare already a clearly separated addition inside this same assembly, not smeared into the core plan-emission code ordinary composable types use. Renders one[ModuleInitializer]-registering,file-scoped registration class (matchingGeneratedTestDoubleRegistry's own precedent for "never referenced by name, staysfile-scoped" per this repo's "Generated code" coding standard) callingglobal::Compono.Logging.LoggingFactoryRegistry.Register<{category}>(static options => new global::Compono.Logging.CapturingLogger<{category}>(options));per discovered closed category — every type referenceglobal::-qualified, per the same standard. -
ComponoIncrementalGenerator.Initializewires this feature in as one additional, clearly-labeled step alongside its existing test-double-eligibility step — not interleaved into unrelated logic.
3. LoggingFactoryRegistry¶
-
public static class LoggingFactoryRegistryinsrc/Compono.Logging—public, notinternal, per ADR-0055 Amendment 2: consumer-generated[ModuleInitializer]code (now emitted by coreCompono.Generators, per Amendment 3, but still landing in the consumer's own assembly) cannot call aninternalmember across an assembly boundary for an arbitrary, unknowable consumer assembly name. This is deliberate, exact precedent, not a new pattern —src/Compono/GeneratedTestDoubleRegistry.csandsrc/Compono/RowInvokerRegistry.csare bothpublicfor this same reason, and this reasoning is entirely unaffected by which generator assembly emits the call (Amendment 3 changes ownership, not this). Shape: - This is a real, deliberate public API addition — record it as such (task 16's public-API validation, below), not as an implementation detail that happens to be
public. - Generator infrastructure, not ordinary consumer-facing API — document it the same way
GeneratedTestDoubleRegistry/RowInvokerRegistryare treated: real and public, but never presented alongsideUseLogging()/CapturingLogger<T>()/CapturedLogEntry/Verify()in normal usage examples. - No
[EditorBrowsable(EditorBrowsableState.Never)]— matches this repo's own explicitly-documented convention (RowInvokerRegistry's doc comment: left undecorated, consistent withPlanCache<T>/CollectionPlanCache<T>/GeneratedTestDoubleRegistry, none of which carry that attribute). - No separate public "bridge"/facade type to keep the real registry
internal—LoggingFactoryRegistryitself is both the storage and the cross-assembly entry point, matchingGeneratedTestDoubleRegistry/RowInvokerRegistryexactly; no evidence justifies a second abstraction here.
The exact backing collection (ConcurrentDictionary<Type, ...> or otherwise) is an implementation detail, not frozen by the ADR — choose based on what the concurrency tests (task 12) actually require, not by default assumption. - [x] Register<TCategory>'s key is typeof(ILogger<TCategory>), computed via an ordinary generic typeof() inside the (statically, per-TCategory) compiled Register<TCategory> method body — confirm this is genuinely a generic-token load, not Type.MakeGenericType, the same way GeneratedTestDoubleRegistry.RegisterFactory<T> already relies on typeof(T) safely. - [x] Idempotent registration (a second Register<TCategory> call for a TCategory already present is a no-op, never a throw/overwrite) — matching GeneratedTestDoubleRegistry's own established behavior for the same cross-module-initializer-ordering reason. - [x] TryCreate is a plain Type-keyed lookup plus one delegate invocation, passing the caller-supplied LoggingOptions through — never captured ahead of time, always the specific UseLogging() configuration active for the request being resolved (task 11). - [x] Test-only guard: nothing in this type or its generated call sites may reference MakeGenericType, Activator.CreateInstance, DynamicMethod, or System.Linq.Expressions — see task 15's source guard for how this gets enforced, not just asserted here.
4. LoggingOptions¶
-
public sealed class LoggingOptions { public LogLevel MinimumLevel { get; set; } = LogLevel.Trace; }— exactly as declared in ADR-0055, no additional members.
5. CapturedLogEntry¶
-
readonly record struct CapturedLogEntrywithLogLevel,EventId,Exception?,Message(string),State(object?),Properties(IReadOnlyList<KeyValuePair<string, object?>>?— the ADR-amended nullable-value signature, not the BCL's own non-nullable-objectshape),MessageTemplate(string?),Scopes(IReadOnlyList<object>),Timestamp(DateTimeOffset). -
Properties/MessageTemplatederivation: a singlestate is IReadOnlyList<KeyValuePair<string, object>> pairspattern match (reflection-free) — the value type on the right-hand side of the pattern match is the BCL's actual non-nullableobject; the publicPropertieslist is materialized asobject?-valued pairs (a boxednullreference assigns without a cast either way, per ADR-0055's "Properties nullability" reasoning).MessageTemplateisProperties's"{OriginalFormat}"entry by key, surfaced asstring?(null ifPropertiesis null or the key is absent). -
Scopesis populated fromLoggerExternalScopeProvider.ForEachScopeat capture time (task 8) — never re-derived after the fact.
6. Internal LogEntryCollector + ICapturingLoggerFacade¶
-
internal sealed class LogEntryCollector— owns: a lock-guardedList<CapturedLogEntry>, oneLoggerExternalScopeProviderinstance, and the effectiveLogLevel MinimumLevel(fromLoggingOptions, fixed at construction — no runtime mutation API). -
IsEnabled(LogLevel level)— implements the exact ADR-0055 rule:level != LogLevel.None && MinimumLevel != LogLevel.None && level >= MinimumLevel. -
Record(LogLevel, EventId, object? state, Exception?, Func<object,Exception?,string> formatter)— checksIsEnabledfirst and no-ops entirely (noCapturedLogEntrybuilt or appended) when disabled; otherwise builds oneCapturedLogEntry(formattedMessagevia the caller's ownformatter,Properties/MessageTemplatevia task 5's pattern match,ScopesviaForEachScope,Timestampvia a single consistent clock source) under the lock, appends it. -
PushScope<TState>(TState state) : IDisposable— forwards toLoggerExternalScopeProvider.Push. - Read/clear surface:
GetEntries() : IReadOnlyList<CapturedLogEntry>(snapshot,.ToArray()/equivalent under the lock),GetLast(),Clear(). -
internal interface ICapturingLoggerFacade— the ADR-0055 "Failure semantics" mechanism. Exposes exactly whatLoggerTestingExtensions(task 9) needs from the collector (e.g. aLogEntryCollector Collector { get; }property) — no more. Implemented by bothCapturingLoggerandCapturingLogger<T>(task 7).
7. CapturingLogger / CapturingLogger<T>¶
Unchanged by any amendment — both remain hand-written, no generated member implementation.
-
public sealed class CapturingLogger : ILogger, ICapturingLoggerFacade— holds oneLogEntryCollector;BeginScope<TState>/IsEnabled/Log<TState>all forward to it. - Public constructor:
CapturingLogger(LoggingOptions? options = null)— per ADR-0055's "Construction semantics" amendment. No category-string parameter (the already-settled "Non-genericILogger" decision stands unchanged). -
public sealed class CapturingLogger<T> : ILogger<T>, ICapturingLoggerFacade— composes an internalLogEntryCollectordirectly (does not contain or delegate to aCapturingLoggerinstance — composition over inheritance per ADR-0055, and there is no shared-base-class shortcut available here since both aresealed). - Public constructor:
CapturingLogger<T>(LoggingOptions? options = null). - Verify (test, not just code): both
LoggingProvider's direct bare-ILoggerpath (task 11) and every generatedLoggingFactoryRegistryactivator (task ⅔) construct captors via these exact same public constructors — one construction path, not a provider/generator-only internal path plus a consumer-facing public path, per ADR-0055's explicit requirement.
8. Scope capture¶
-
LoggerExternalScopeProvider(namespaceMicrosoft.Extensions.Logging) is the sole scope mechanism — no custom scope stack.BeginScope<TState>returns the realIDisposablefromPush, unmodified. -
Record(...)(task 6) snapshots viaForEachScopeintoCapturedLogEntry.Scopesat the moment of the log call, before the entry is appended — never re-queried later.
9. LoggerTestingExtensions + non-Compono-logger failure semantics¶
-
GetCapturedEntries(this ILogger logger),GetLastCapturedEntry(this ILogger logger),ClearCapturedEntries(this ILogger logger): each pattern-matcheslogger is ICapturingLoggerFacade facade; on match, forwards to the facade's collector; on non-match, throwsInvalidOperationExceptionwith a message naming the concreteloggertype, stating it is not aCompono.Loggingcapturing logger, and pointing atUseLogging()and the stage-6 registration-order requirement as the likely cause. -
Verify(this ILogger logger) : LogVerificationBuilder— same facade check and same failure behavior as the three methods above. - Keep this exception distinct from task 11's missing-generated-activation exception — same
InvalidOperationExceptiontype, but the two must have clearly different messages/causes (this one: "not a Compono.Logging logger at all, likely wrong provider order"; task 11's: "recognized asILogger<T>but no generated activation exists, likely a generator/discovery gap"). Cover both in the same focused test file so the distinction is obvious to a future reader (task 13).
10. LogVerificationBuilder + CallVerifier reuse¶
-
AtLevel(LogLevel),WithEventId(EventId),WithException<TException>() where TException : Exception,WithMessageContaining(string),Matching(Func<CapturedLogEntry, bool>)— each accumulates a filter predicate (an internal composedFunc<CapturedLogEntry, bool>, not a separate expression tree/matcher type); no filter is applied until a terminal call. -
Once()/Never()/Exactly(int)— each snapshots the collector's current entries (task 6'sGetEntries()), applies the accumulated filter, counts matches, and constructs one existingCompono.CallVerifierfrom that count plus a description built from the accumulated filters — then calls the correspondingCallVerifiermember. No new count/Times abstraction anywhere in this type;CallVerifieritself is never part ofLogVerificationBuilder's public surface.
11. LoggingProvider + UseLogging()¶
-
internal sealed class LoggingProvider(LoggingOptions options) : ICompositionValueProvider—TryProvide: - Bare
ILogger(RequestedType == typeof(ILogger)): returnsSuccesswithnew CapturingLogger(options)directly — no registry, no generated bridge, since there's no generic parameter to close. - Closed
ILogger<T>(RequestedType.IsGenericType && RequestedType.GetGenericTypeDefinition() == typeof(ILogger<>)— the same staticTypecheck patternNSubstituteProvideralready uses): queryLoggingFactoryRegistry.TryCreate(request.RequestedType, options, out var value).- Found: returns
Successwithvalue. - Not found: throws immediately with the ADR-0055-specified
InvalidOperationExceptionnaming the requested closed type, stating no generated activation was available, and pointing at source-generation/discovery coverage (including the ADR-0052 Finding-B cross-reference where relevant) as the likely cause. Never returnsNotHandledfor this case — falling through would let a laterUseNSubstitute()/UseGeneratedTestDoubles()registration silently mask a real generator/discovery gap.
- Found: returns
- Anything else:
NotHandled, never throws. -
public static CompositionBuilder UseLogging(this CompositionBuilder builder, Action<LoggingOptions>? configure = null)— builds aLoggingOptions, appliesconfigureif given, callsbuilder.AddTestDoubleProvider(new LoggingProvider(options))(stage 6, ADR-0024's existing extension point — no new engine mechanism). - Each composed request gets its own independent
CapturingLogger/CapturingLogger<T>by default (Compono's independent-by-default model) — confirm no accidental sharing across two distinct requests for the same closed type absent[Shared].
12. Concurrency¶
-
LogEntryCollector's backing store is lock-guarded; every read (GetEntries(),GetLast(),Verify()'s internal snapshot) takes a point-in-time copy, never a live view — matchingCompono.Http's already-establishedTestHttpHandler.Requestspattern. -
Clear()and concurrentLog<TState>calls are safe under the same lock; a concurrency test (many parallelLog<TState>calls against one collector, task 13) proves no lost entries and no exception under contention. -
LoggingFactoryRegistry's own concurrent registration/lookup (many module initializers racing at assembly-load time, concurrentTryCreatelookups during composition) is safe — choose and justify the backing collection here, per task 3's note.
13. Tests (test/Compono.Logging.Tests)¶
Structured-state: - [x] logger.LogInformation("...", args) — FormattedLogValues path: Message, State, Properties, MessageTemplate, original boxed CLR property types preserved (not stringified). - [x] A [LoggerMessage]-source-generated logging call (a small partial-method fixture in the test project) — LoggerMessageState path: same five assertions as above, proving the single pattern-match covers both call styles with no special-casing. - [x] A structured argument that is null (e.g. LogInformation("User {UserId}", (int?)null)) — Properties correctly carries a null value, no exception.
MinimumLevel (ADR-0055's explicit validation expectations): - [x] MinimumLevel = LogLevel.Trace (default) — every ordinary level Trace-Critical is enabled and captured. - [x] MinimumLevel = LogLevel.Warning — Trace/Debug/Information are neither enabled nor captured; Warning/Error/Critical are captured. - [x] MinimumLevel = LogLevel.None — no ordinary level is enabled; nothing is ever captured regardless of what's logged. - [x] A direct Log<TState> call passing LogLevel.None captures nothing, independent of MinimumLevel's value (including MinimumLevel = Trace). - [x] A disabled-level call does not appear in GetCapturedEntries(), is never returned by GetLastCapturedEntry(), and does not count toward Verify()'s match count for Once()/Never()/Exactly(n).
Scope: - [x] No active scopes — Scopes is empty. - [x] Single scope — Scopes has exactly that one entry. - [x] Nested scopes (3 levels) — Scopes is outermost-to-innermost. - [x] Dispose/pop behavior — disposing an inner scope removes only that scope from subsequent entries; disposing the outer scope after the inner clears both. - [x] Flow across await — a scope pushed before an await in the same logical call is still visible to a log call after the await. - [x] Expected ExecutionContext flow — a sibling Task.Run started before the scope push does not see it; one started after does. - [x] The scope snapshot on CapturedLogEntry.Scopes is fixed at the moment of the log call — a scope pushed or disposed afterward does not retroactively change an already-captured entry.
Verification API (full public shape): - [x] logger.Verify().AtLevel(...).WithEventId(...).WithException<T>().WithMessageContaining(...).Matching(...).Once() — end-to-end, all five filters combined, matching exactly one entry. - [x] Never() — zero matches passes, one or more matches throws. - [x] Exactly(n) — exact-count match passes; any other count throws. - [x] Each filter in isolation narrows correctly against a mixed set of captured entries. - [x] Confirm (via the actual Compono.CallVerifier type, not a reimplementation) that Once/Never/Exactly failures throw TestDoubleVerificationException with the filter description in the message.
Provider/precedence/activation: - [x] Bare ILogger composes through UseLogging() (direct-construct path, no registry involved). - [x] ILogger<T> composes through UseLogging() for an arbitrary T discovered by the (now-shared) generator (task 2/14) — this is the primary, realistic end-to-end path and must be exercised via a real composed root, not a hand-called LoggingProvider.TryProvide. - [x] LoggingProvider.TryProvide returns NotHandled for an unrelated type (e.g. IOrderRepository) — never throws. - [x] UseLogging() registered before UseGeneratedTestDoubles()/ UseNSubstitute() — ILogger<T> resolves to a CapturingLogger<T>. - [x] Reverse registration order — ILogger<T> resolves to whatever the other provider produces — confirms the existing first-registered-wins pipeline behavior is unchanged, not a new diagnostic. - [x] Calling logger.Verify()/GetCapturedEntries()/etc. on an ILogger<T> produced by the other provider (the reverse-order case above) throws task 9's InvalidOperationException ("not a Compono.Logging logger"), not an InvalidCastException or a silently empty result. - [x] Missing-generated-activation: construct a LoggingProvider scenario where RequestedType is closed-ILogger<T>-shaped but LoggingFactoryRegistry has no entry for it — confirm it throws task 11's distinct InvalidOperationException, and confirm it does not fall through to NotHandled/another provider.
Direct construction: - [x] new CapturingLogger() and new CapturingLogger(options) both work standalone, no composition involved. - [x] new CapturingLogger<T>() and new CapturingLogger<T>(options) likewise — this needs no generated activation bridge at all, since T is statically known at the call site (confirm this is exercised without ever touching LoggingFactoryRegistry). - [x] A directly-constructed logger and a LoggingProvider/registry- produced logger for the same T/options exhibit identical IsEnabled/capture/scope/Verify() behavior — same code path, not two implementations.
Concurrency: - [x] Many parallel Log<TState> calls against one collector — no lost entries, no exception, final count matches call count. - [x] GetCapturedEntries()/Verify() reads concurrent with in-flight Log<TState> calls never throw and never return a corrupted/partial entry.
14. Logging-generator tests (existing test/Compono.Generators.Tests)¶
No new test project. These are new test cases added to the existing test/Compono.Generators.Tests (mirroring its established shape: compile a small in-memory Compilation, run ComponoIncrementalGenerator, assert on emitted source and/or diagnostics), covering the full ComponoGeneratedLogging gating matrix from ADR-0055 Amendment 3 plus discovery correctness:
Property gating (the matrix Amendment 3 specifies exactly): - [x] ComponoGeneratedLogging absent/unset, Compono.Logging not referenced → no logging-specific discovery or emission, no diagnostic. - [x] ComponoGeneratedLogging absent/unset, Compono.Logging referenced with its packed default-value props simulated (test harness sets the property to true, matching what the package's own default would produce) → generation enabled, reachable ILogger<T> activation emitted. - [x] Explicit ComponoGeneratedLogging=false, Compono.Logging referenced → generation disabled, no emission, no diagnostic — the explicit override wins over the (simulated) package default. - [x] Explicit ComponoGeneratedLogging=true, Compono.Logging referenced, all three required symbols resolvable → normal discovery/emission. - [x] Explicit ComponoGeneratedLogging=true, Compono.Logging not referenced (required symbols unresolvable) → the dedicated CMP0038-or-whatever-the-confirmed-id-is diagnostic reported, no generated registration source emitted at all (not partial, not broken). - [x] ILogger<T> available in the compilation (i.e. Microsoft.Extensions.Logging.Abstractions referenced) but ComponoGeneratedLogging disabled → no logging emission, regardless of ILogger<T> presence. - [x] A compilation that references neither Microsoft.Extensions.Logging.Abstractions nor sets the property at all → zero logging-generator output, zero errors — proves the GetTypeByMetadataName short-circuit actually works, not just compiles.
Discovery correctness (reusing the existing pipeline): - [x] ILogger<T> directly on a composed root's constructor — registration emitted for T. - [x] Multiple distinct ILogger<T> categories in one compilation — one registration per category, all emitted. - [x] A nested, transitive constructor dependency (root → composable → ILogger<Leaf> parameter) — registration emitted for Leaf, proving the existing recursive walk correctly carries this new leaf shape. - [x] [Composable]-marked root (both direct-attribute and assembly-level forms) reaching an ILogger<T> dependency. - [x] [Compose]/[Compose<TProfile>] theory-row parameter typed ILogger<T> (or reaching one transitively). - [x] Composer.Create<T>()/CreateMany<T>() roots reaching an ILogger<T> dependency. - [x] The same closed T discovered via two different roots emits exactly one registration, not two. - [x] An ILogger<T> constructor parameter on a type that is not reachable from any real composition root produces no registration for it — confirms this rides the existing "real roots only" discovery model, not a compilation-wide scan. - [x] Bare, non-generic ILogger requires no generated registration at all. - [x] Generated source uses a statically closed CapturingLogger<T> activation (new CapturingLogger<T>(options) inline in generated code) — inspect the emitted source text, not just that it compiles. - [x] Generated activator delegate signature accepts LoggingOptions as a runtime parameter (not captured, not zero-argument) — inspect the emitted source text for this shape specifically. - [x] Boundary case, explicitly documented as intentionally unsupported, not accidentally missed: an ILogger<T> dependency reachable only through a hand-written Register<T>(...) factory's own internal context.Resolve<ILogger<TSomething>>() call (ADR-0052 Finding-B shape) produces no registration — assert this is the observed (expected) behavior with a comment/test name that makes clear this is a known, ADR-0052-cross-referenced limitation, not a bug to fix in this plan. - [x] Confirm existing, unrelated Compono.Generators.Tests coverage (ordinary composition plans, collection plans, TestDoubles generation) is unaffected — this feature must be additive, never change any existing generator output for a compilation that doesn't reference Compono.Logging.
15. Native AOT/trimming validation (test/Compono.Logging.AotSmokeTest)¶
- The AOT smoke consumer project must exercise the generated composition path, not only direct construction: a real composed root with an
ILogger<T>constructor dependency, composed viaComposer.Create(builder => builder.UseLogging()), such that the (shared) generator actually emits an activation for it. Directnew CapturingLogger<T>()construction alone does not exercise the thing this feature exists to prove. - Validate, for that consumer: generator output is present; the composed
ILogger<T>resolves successfully at runtime;GetCapturedEntries()/Verify()work correctly against it. -
dotnet publishwithPublishAot/IsAotCompatible=truesucceeds for that consumer. - Zero
IL2026/IL3050/trim-analyzer warnings attributable toCompono.Logginganywhere in that build (mirroringCompono.Http.AotSmokeTest's two-proof pattern: (a) the consumer build, (b)Compono.Logging's own build with its analyzer turned on, proving no reflection-related warning exists there). - Source-level guard (not just "it built clean this time"): a focused check — a simple text/syntax scan over
src/Compono.Logging/**/*.csand the new logging-activation files insidesrc/Compono.Generators/**(not a separate project, per Amendment 3 — point this at the actual files added there) — that fails the build ifMakeGenericType,Activator.CreateInstance,DynamicMethod, orSystem.Linq.Expressionsever appears in either location, so a future change can't silently reintroduce the reflection path this design specifically rejected. - Confirm no reflection fallback is reachable from any public entry point — there is no code path (not even an error path) in
LoggingProvider/LoggingFactoryRegistrythat falls back to reflection-based activation when the registry lookup misses; task 11 already specifies it throws instead.
16. Documentation and skill/reference synchronization (completion-gate work, not cleanup)¶
Each item states what the surface must learn, not just "update it":
-
docs/packages/compono-logging.md(new) — followingdocs/packages/compono-http.md's exact shape: an inventory/"when to install" section, a workedUseLogging()+[Shared]+Verify()/GetCapturedEntries()example (this is also where ADR-0055's usage examples live — see "Usage-example convention" below), structured-Properties/MessageTemplatebehavior, scope semantics (outermost-to-innermost, async-safe),MinimumLevel/LogLevel.Nonefiltering semantics, direct-construction support, the non-Compono-loggerInvalidOperationExceptiondiagnostic, the explicit v1 boundary list (noILoggerFactory, no Serilog-specific behavior, etc.), and, per Amendments ⅓: that installingCompono.Loggingenables its generation behavior by default (ComponoGeneratedLogging, defaulting totruevia the package's own MSBuild props asset), how to explicitly opt out (<ComponoGeneratedLogging>false</ComponoGeneratedLogging>), that the actual generation happens inside the sharedCompono.Generatorsassembly (not a package-specific generator), thatCapturingLogger/CapturingLogger<T>are hand-written (the generator emits activation glue only), that discovery follows reachable Compono composition roots (not a blanket scan), the static-discoverability limitation (with the ADR-0052 Finding-B example), the missing-generated-activation runtime diagnostic, the missing-runtime-symbols compile-time diagnostic (CMP0038, ifComponoGeneratedLoggingis forcedtruewithoutCompono.Loggingreferenced), and that no reflection fallback exists anywhere. -
docs/packages/index.md— add aCompono.Loggingrow matching the existing table shape. -
README.md— add aCompono.Loggingrow to the package/badge table (mirrors the existingCompono.Httprow, lines 39-46). -
docs/architecture/current/provider-pipeline.md— extend the stage-6 row (line 20) to nameCompono.Logging'sLoggingProvideralongsideNSubstituteProvider/GeneratedTestDoubleProvider, and add the explicitUseLogging()-before-UseNSubstitute()/UseGeneratedTestDoubles()precedence sentence. -
docs/architecture/current/generated-plans-and-discovery.md— during planning, inspect whether this is the authoritative place to record thatCompono.Generatorsnow also emits logging activation (a second, gated feature alongsideComponoGeneratedTestDoubles), and if so, add a short cross-reference to ADR-0055 Amendment 3 anddocs/packages/compono-logging.mdrather than duplicating the discovery-model explanation. If a different existing doc is found to be the actual authoritative home, name that file instead. -
docs/public-api.md— this file is a tombstone/redirect (ADR-0030 Amendment 2); its only relevant content is the bullet listing every package (line 18) — addCompono.Loggingto that list. - Usage examples — resolved location, see below — no separate samples project; examples live inline in
docs/packages/compono-logging.md, consistent withCompono.Http's own convention. -
skills/compono/SKILL.md— three concrete edits: (1) addCompono.Loggingto the package-enumeration sentence (line 8); (2) add a.csproj-detection row to the definitive-signal table (lines 50-54) —<PackageReference Include="Compono.Logging"→UseLogging()available (generation on by default), loadreferences/logging.md; (3) add areferences/logging.mdrow to the references-index table (lines 365-372), matching thereferences/http.mdrow's phrasing. -
skills/compono/references/logging.md(new) — covering, at minimum:UseLogging(), direct construction, structuredProperties/MessageTemplateextraction, scope semantics,Verify()'s full fluent shape,MinimumLevel/LogLevel.Nonefiltering, theUseLogging()-before-UseNSubstitute()/UseGeneratedTestDoubles()registration-order requirement, the non-Compono-loggerInvalidOperationExceptiondiagnostic, the explicit v1 boundary list, and, per Amendments ⅓: default-on generation viaComponoGeneratedLoggingand how to opt out, that generation happens inside the sharedCompono.Generators, that closedILogger<T>composition requires the type to be reachable from a real composition root, the missing-generated-activation and missing-runtime-symbols diagnostics' distinct meanings — matchingreferences/http.md's existing depth and structure. -
skills/compono-evals/evals.json— add at least one new eval requiring ordinaryILogger<T>composition throughUseLogging()(not merely directnew CapturingLogger<T>()construction), following the existing eval shape and asserting real API usage with no invented API or reflection-based workaround suggested. Add a second eval covering the registration-order precedence pitfall if the benchmark format supports a diagnosis-style prompt, matching eval #2's existing shape. -
skills/compono-evalsbenchmark/grading artifacts — run the existing before/after benchmark harness for the new eval(s) against the updated skill, recording the result per that directory's established convention. -
docs/adr/README.md— already updated (ADR-0055's own index row); no further change needed here. -
LoggingFactoryRegistrydocumentation treatment (ADR-0055 Amendment 2) — note it exists (real, public, generator infrastructure) but must not be presented alongsideUseLogging()/CapturingLogger<T>()/Verify()/GetCapturedEntries()as something a consumer is expected to call directly. Do not add[EditorBrowsable(Never)]to compensate in code. - Public-API/reference regeneration (
docs/reference/api) — per ADR-0032, regeneratedocs/reference/api/Compono.Logging/as part of this PR so it includes every shipped public member, explicitly includingLoggingFactoryRegistry.Register<TCategory>/TryCreate. -
Compono.Generators' own diagnostic docs — wherever this repo'sCMP00xxdiagnostics are documented for consumers (references/diagnostics.mdperskills/compono/SKILL.md's own references-index table), add the new missing-runtime-symbols diagnostic alongside the existingCMP0020-CMP0032entries.
17. Package creation and MSBuild default-property asset¶
- Core
Compono's existingsrc/Compono/build/Compono.propsgains one more declaration, alongside the existingComponoGeneratedTestDoublesone: Inert for any consumer who never referencesCompono.Logging. No new file — extend the existing one. - New
src/Compono.Logging/build/Compono.Logging.props, packed to bothbuild/Compono.Logging.propsandbuildTransitive/Compono.Logging.propsinsideCompono.Logging.nupkg(mirroringCompono.csproj's existing two-<None>-entries pattern forbuild/Compono.props), containing only: -
Compono.Logging.nupkgcontains the runtime assembly, its normal dependencies (Compono,Microsoft.Extensions.Logging.Abstractions), and this tiny props asset — no generator/analyzer DLL of any kind. Confirm directly (e.g. inspect the built.nupkg's contents /.nuspec), not by assumption. -
Compono.nupkgremains the sole owner ofCompono.Generators.dll— confirm its packed contents are unchanged in shape (still exactly one analyzer DLL, now with new logging-activation behavior inside it). - Packaging tests (new, part of this task group's own validation, not folded into task 13/14): confirm the packed
Compono.Logging.nupkgcontains the expectedbuild/buildTransitiveprops assets and confirm it contains noanalyzers/folder at all; confirm the packedCompono.nupkgstill contains exactly the one sharedCompono.Generators.dllanalyzer asset (no duplicate, no second copy anywhere). -
<IsAotCompatible>true</IsAotCompatible>preserved onCompono.Logging.csproj(task 1) through the packed artifact. - Confirm, directly (a real local pack + restore into a scratch consumer, not assumed), that a consumer referencing only
Compono.Logging(which pullsComponoin transitively) receives bothCompono.Generators.dll's execution andComponoGeneratedLogging's default-truevalue with zero extra steps.
18. Dogfood migration (separate PR(s), after Compono.Logging ships — see "Status contract")¶
Pack Compono + Compono.Logging (and any other package a given consumer already uses) into the local feed via scripts/dogfood-validate.sh --packages "Compono Compono.Logging ..." against each consumer's real test suite, using freshly built local packages.
LayeredCraft.StructuredLogging: - [x] Inventory every real use of TestLogger/TestLogger<T>/ TestingExtensions (WarningExtensionsTests.cs and any sibling test files) before changing anything. - [x] Where a test currently does new TestLogger() and could instead compose [Compose] ILogger (or ILogger<T>) via UseLogging(), migrate it — but only where doing so doesn't just recreate TestingExtensions' own assertion helpers under a different name. - [x] Explicitly decide, and record the decision, on LayeredCraft.StructuredLogging's own Testing/TestingExtensions.cs: deprecate/remove in favor of Compono.Logging, or keep it.
alexa-vox-craft: - [x] test/AlexaVoxCraft.MediatR.Tests/TestKit/MediatRTestProfile.cs's Register<ILogger<PerformanceLoggingBehavior>>(() => new TestLogger<PerformanceLoggingBehavior> { ... }) → remove the manual registration; let UseLogging() compose it; confirm PerformanceLoggingBehaviorTests.cs's assertions still pass, migrated to Compono.Logging's Verify()/GetCapturedEntries() API. - [x] Register<ILogger<SkillMediator>>(() => NullLogger<SkillMediator>.Instance) → confirm whether this fallback is still needed once UseLogging() is active. - [x] Distinguish genuine remaining consumer-specific architecture from a missing Compono.Logging capability — report the latter rather than redesigning the consumer repo to force a fit. - [x] Do not modify either consumer repository as part of this plan's drafting — this task group only executes once Compono.Logging has shipped and been packed.
19. ILogger/ILogger<T> TestDoubles-exclusion (ADR-0055 Amendment 4)¶
Found during task 18 dogfooding against alexa-vox-craft: with both ComponoGeneratedTestDoubles=true and ComponoGeneratedLogging=true on the same ILogger<T>, Compono.TestDoubles' generated, exact-typed Verify(this ILogger<T>) extension wins C# overload resolution over Compono.Logging's Verify(this ILogger), silently breaking Compono.Logging's verification API. Root-caused, proven via throwaway spike (reverted — nothing from the spike is reused directly, this task implements it for real), and resolved by ADR-0055 Amendment 4 (Accepted). This task group is the real, permanent implementation of that amendment — do the narrow fix only; no generalized generator feature-ownership/provider-priority mechanism.
-
LeafTypeClassifier/TransitiveClosureWalker: whenComponoGeneratedLoggingis enabled, excludeMicrosoft.Extensions.Logging.ILoggerand any closedMicrosoft.Extensions.Logging.ILogger<T>fromCompono.TestDoubles-eligibility (TryRecordTestDouble), reusingLoggingWellKnownTypes/ctx.LoggingWellKnownalready threaded throughWalkContextforTryRecordLoggingCategory. No new well-known-type resolution needed. ClosedILogger<T>still gets Logging's generated activation exactly as before (Amendments ⅓, unchanged); bareILoggergets no generated anything either way (Amendment 4's "no-op half" of the rule) — implement the ownership check uniformly across both shapes anyway, per the amendment's "one rule, not two" reasoning. - Permanent generator tests (
test/Compono.Generators.Tests/, replacing the throwawaySPIKE_Amendment4CollisionTests.csshape with real, kept tests) covering the four-way matrix:ComponoGeneratedTestDoubles×ComponoGeneratedLogging∈ {off,on}×{off,on}, for both closedILogger<T>and bareILogger. Assert generated-tree text presence/absence ofLoggingFactoryRegistryregistration vs. a TestDoubles_DoubleVerifier/Verify()extension for that exact type, matching the spike's four proven cases (TestDoubles+Logging on together → Logging activation only, no competing extension; TestDoubles on/Logging off → existing double behavior preserved unchanged; both off → neither; bareILogger→ no TestDoubles double either way, both before and after this task, recorded as a regression guard for the already-true no-op). - Compile-level regression (
test/Compono.Logging.Tests/or a new test project mirroringalexa-vox-craft's real shape, bothComponoGeneratedTestDoubles=trueandComponoGeneratedLogging=true, an interface and anILogger<T>reachable from the same composition root) provinglogger.Verify().AtLevel(...)actually compiles and binds toCompono.Logging'sLogVerificationBuilder— not just that the right generated text exists, but that a real consumer-shaped compilation resolves the overload correctly end to end. - Regression: existing
Compono.TestDoubles-only behavior forILogger<T>(noCompono.Loggingreferenced, orComponoGeneratedLogging=false) is unchanged — reuse/extendLoggingGeneratorTests' existing property-gating tests rather than duplicating them. - Documentation/skill updates so this interoperability rule is documented, not silent generator behavior:
skills/compono/references/logging.md's "Registration order (stage-6 precedence)" section anddocs/packages/compono-logging.md's identically-named section: replace the blanket "UseLogging()must precede bothUseNSubstitute()andUseGeneratedTestDoubles()" framing with Amendment 4's corrected statement — registration order betweenLoggingProvider/GeneratedTestDoubleProvideris no longer observable forILogger/ILogger<T>onceComponoGeneratedLoggingis enabled (no generated factory exists for those types anymore);UseLogging()must still precedeUseNSubstitute()(and any other provider that can independently produce anILogger/ILogger<T>substitute).skills/compono/references/testdoubles.md: note theILogger/ILogger<T>exclusion whenCompono.Logging's generation is enabled, cross-referencing ADR-0055 Amendment 4, so a reader of the TestDoubles reference isn't surprised these two types are absent from generation under that condition.docs/architecture/current/generated-plans-and-discovery.md: extend the existing "Other compile-time-gated generation in this same assembly" paragraph to note the two discovery buckets are now mutually exclusive forILogger/ILogger<T>, linking Amendment 4.- Re-run the full existing regression surface after the real change lands:
test/Compono.Generators.Tests(all, not just the new/logging ones),test/Compono.Logging.Tests,test/Compono.TestDoubles.Tests(confirm no non-ILoggerinterface regresses),test/Compono.Tests. - Re-run PLAN-0055 task 18's dogfooding against
alexa-vox-craftfrom freshscripts/dogfood-validate.sh-packed local packages (not ProjectReferences, not stale packages) once the real fix is in — this is the task that was blocked by the collision; it resumes here rather than being separately re-authorized.
Critical Files¶
New: - src/Compono.Logging/Compono.Logging.csproj - src/Compono.Logging/build/Compono.Logging.props (new — the default- value MSBuild props asset, task 17) - src/Compono.Logging/LoggingOptions.cs - src/Compono.Logging/CapturedLogEntry.cs - src/Compono.Logging/LogEntryCollector.cs (internal) - src/Compono.Logging/ICapturingLoggerFacade.cs (internal) - src/Compono.Logging/CapturingLogger.cs - src/Compono.Logging/CapturingLogger{T}.cs - src/Compono.Logging/LoggerTestingExtensions.cs - src/Compono.Logging/LogVerificationBuilder.cs - src/Compono.Logging/LoggingProvider.cs (internal) - src/Compono.Logging/LoggingFactoryRegistry.cs (public — generator infrastructure, per ADR-0055 Amendment 2; not consumer-facing usage surface) - src/Compono.Logging/CompositionBuilderExtensions.cs - test/Compono.Logging.Tests/* - test/Compono.Logging.AotSmokeTest/* - docs/packages/compono-logging.md - skills/compono/references/logging.md
Modified (existing Compono.Generators, per ADR-0055 Amendment 3 — no new generator project): - src/Compono.Generators/WellKnownTypes/ — new optional ILogger/ILogger1resolution. -src/Compono.Generators/Discovery/LeafTypeClassifier.csand/orTransitiveClosureWalker.cs— the one new per-parameter-type check hooked into the existing walk. -src/Compono.Generators/Emitters/— newLoggingActivationEmitter.cs(or similarly named), clearly separated from existing emitters. -src/Compono.Generators/Templates/— newLoggingActivation.scriban. -src/Compono.Generators/ComponoIncrementalGenerator.cs— one new, clearly-labeled wiring step (property read, discovery hook, emission call). -src/Compono.Generators/Diagnostics/— the new missing-runtime-symbols diagnostic descriptor. -src/Compono.Generators/AnalyzerReleases.Unshipped.md— new row. -src/Compono/build/Compono.props— newComponoGeneratedLoggingCompilerVisiblePropertydeclaration. -test/Compono.Generators.Tests/*` — new test cases (task 14), no new test project.
Modified (docs/skills): - docs/packages/index.md, README.md, docs/public-api.md, docs/architecture/current/provider-pipeline.md, and (pending planning-time confirmation, task 16) docs/architecture/current/generated-plans-and-discovery.md - skills/compono/SKILL.md, skills/compono/references/diagnostics.md, skills/compono-evals/evals.json and its benchmark/grading artifacts - The solution file(s) referencing src/Compono.*/test/Compono.* projects (src/Compono.Logging and test/Compono.Logging.Tests/ Compono.Logging.AotSmokeTest only — no second generator project)
Later (dogfood PRs, separate repos): - layered-craft/structured-logging: src/LayeredCraft.StructuredLogging/Testing/TestingExtensions.cs and its test call sites. - ncipollina/alexa-vox-craft: test/AlexaVoxCraft.MediatR.Tests/TestKit/MediatRTestProfile.cs, test/AlexaVoxCraft.MediatR.Tests/Pipeline/PerformanceLoggingBehaviorTests.cs.
Test Plan¶
Per references/testing.md's conventions: behavioral unit tests in test/Compono.Logging.Tests covering every bullet in task 13 (structured-state, MinimumLevel/LogLevel.None, scope, verification API, provider/precedence/activation, direct construction, concurrency); new generator test cases added to the existing test/Compono.Generators.Tests (task 14) covering the full ComponoGeneratedLogging property-gating matrix plus discovery correctness; a dedicated AOT smoke project (task 15) proving the generated composition path is trim/reflection-clean, plus a source-level guard against MakeGenericType/ Activator.CreateInstance/dynamic-code activation across both Compono.Logging and the new logging-activation files inside Compono.Generators; packaging tests (task 17) proving Compono.Logging.nupkg carries no analyzer and Compono.nupkg carries exactly one, unchanged Compono.Generators.dll; the repo's full solution test sweep (dotnet test across every existing project) run before considering this plan done, to catch any regression the new stage-6 registrant or generator addition introduces elsewhere; the compono-evals before/after benchmark run for the new eval(s) (task 16); and, per the Status contract above, scripts/dogfood-validate.sh runs against both LayeredCraft.StructuredLogging and alexa-vox-craft using freshly packed local Compono/Compono.Logging packages (task 18) as part of this plan reaching Status: Done, even though that work lands as separate PRs in those repos.
Validation Gate (must all pass before Status: Done)¶
- Focused unit tests (task 13) — all green.
- Logging-generator tests in
Compono.Generators.Tests(task 14) — all green, including the full property-gating matrix and the negative/unreachable-type case. - Integration/composition tests (provider/precedence/activation subset of task 13) — all green, including the real generator-discovered
ILogger<T>end-to-end path, not only direct construction. - Concurrency tests (task 12/13) — all green.
- Structured-state tests (both
LogInformationand[LoggerMessage]paths) — all green. - Scope tests (ordering, dispose, async flow) — all green.
- Provider-precedence tests (both registration orders, the wrong-provider
InvalidOperationExceptioncase, and the missing-generated-activationInvalidOperationExceptioncase, clearly distinguished from each other) — all green. - Direct-construction tests — all green, and confirmed identical behavior to provider/registry-produced loggers.
- Native AOT/trimming smoke (task 15) — zero warnings on both proofs, proving the generated path, plus the source-level reflection guard passing across both
Compono.Loggingand the newCompono.Generatorsfiles. - Full solution test sweep — no regression anywhere else in the repo, including every existing
Compono.Generators.Testscase that doesn't touch logging. - Documentation verification — every file named in task 16 actually updated and internally consistent with the shipped API, including the default-on-generation/opt-out behavior.
- Skill/reference synchronization —
skills/compono/SKILL.md+references/logging.md+references/diagnostics.mdreviewed against the real shipped API. -
compono-evalsCompono.Logging eval(s) passing against the updated skill, including the ordinary-UseLogging()-composition eval. - Packaging tests (task 17) —
Compono.Logging.nupkgconfirmed analyzer-free with its props asset present;Compono.nupkgconfirmed unchanged in analyzer-asset shape. -
scripts/dogfood-validate.shpassing against bothLayeredCraft.StructuredLoggingandalexa-vox-craft(task 18, separate PR(s) against those repos — required for this plan's ownStatus: Done, per the Status contract above and PLAN-0051's real precedent).
If any implementation step surfaces evidence that contradicts ADR-0055's decisions (including its amendments — e.g. the shared generator's existing walk turning out not to carry the new ILogger<T> leaf shape cleanly, or GetTypeByMetadataName behaving unexpectedly for the generic ILogger<> form, or the MSBuild default-property mechanism not actually overriding as designed), stop and report it rather than silently diverging from the accepted ADR.
Usage-example convention (resolved during planning)¶
Inspected docs/packages/compono-http.md (the direct precedent for a runtime, non-generated integration package) and the samples/ directory (Compono.Samples.AspNetApi, Compono.Samples.AspNetApi.Tests, Compono.Samples.BasicUsage — the ADR-0033 public-preview samples strategy, a separate concern from per-package API documentation). Compono.Http has no entry in samples/ — its usage examples live entirely inline in docs/packages/compono-http.md. Compono.Logging follows the same, already-established convention: its usage examples live inline in the new docs/packages/compono-logging.md, not as a new samples/ project. No samples project is added by this plan.
Notes¶
(Updated as implementation proceeds — empty at plan creation time.)