[RESEARCH-0029] Post-1.2 Capability Admission Research¶
Status: Done (research only; no ADR/Plan created). Revised 2026-09-11 per explicit product-owner direction after reviewing the original pass — see "Revision (2026-09-11)" immediately below. Updated in place, per instruction, rather than issued as a new research number.
Governing process: docs/architecture/capability-admission.md, applied per candidate. Trigger: general request to identify legitimate post-1.2 capability candidates for Compono — not tied to a specific consumer ask. Compono is currently at v1.2.1. "No feature needed" was an explicitly valid outcome going in.
Revision (2026-09-11): product-owner direction¶
Three findings and one correction from the original pass, below, prompted explicit product-owner decisions. Sections carrying a (Revised 2026-09-11) marker were changed; everything else is the original 2026-09-09 research, unchanged.
Correction: the original pass looked for cosmere-tracker at /Users/ncipollina/source/repos/layered-craft/cosmere-tracker (wrong) and concluded it no longer exists. Its actual path is /Users/ncipollina/source/repos/ncipollina/cosmere-tracker — a different owner segment, not a moved/deleted repo. It was re-inspected read-only for this revision; see the rewritten §4.
Explicit admission decisions (Gate B satisfied by product-owner request in its own right, independent of — and not defeated by — the thin or absent dogfooding evidence the original pass found):
- HTTP body matching — ADMITTED. Goal:
Compono.Httpshould let a consumer verify actual outgoing request content without sync-over-async, a weak content-type-only check, manual buffering, or a generic opaque predicate. API shape is explicitly not decided —WhenAsync/WithBody/WithJsonBody<T>/WithFormBodyare possibilities the owner named, not requirements. - HTTP header matching — ADMITTED, independently of dogfooding friction. Headers are a normal request-matching dimension for a package that already owns request matching as its core responsibility. Whether body and header matching should be one coherent fluent model or separate bolted-on helpers is an open design question, not pre-decided either way.
Compono.Loggingstructured verification — ADMITTED. The package already captures message templates and structured properties;Verify()can't express them directly today, forcing a.Matching(...)fallback with weak diagnostics. API shape not decided —WithProperty/WithMessageTemplateare starting points, not a locked design.- Options verification — not requested. Prior rejection (§5, Candidate 4) stands; the corrected
cosmere-trackerinspection (§4) is checked explicitly for evidence that would change this, and found none. Compose<TProfile, TConfig>/profile ergonomics — owner agrees no new runtime API is needed. Real usage with a coding agent showed it trying to hand-construct/wrap Options dependencies instead of recognizing the Profile +Compose<TProfile, TConfig>+CompositionBuilder+Compono.Optionspattern — this is doc/skill work, addressed concretely in the revised Candidate 5 below, plus a designed (not yet added) skill eval scenario.- Diagnostics (
CS9057) — the doc/skill finding stands as originally scored; explicitly not to be allowed to crowd out the three admitted capabilities above. The speculative generator-non-execution detector stays research-only.
This revision does not mean these three capabilities get bundled into one forced release, and does not mean their APIs are now locked — Gate B admission (a product decision) is distinct from API design (still fully open for all three) and from implementation planning (not started for any). See the new §5a design-readiness assessment for what's actually ready for an ADR versus what needs a short design spike first.
1. Executive summary (Revised 2026-09-11)¶
Three capabilities are now admitted by explicit product-owner decision, independent of dogfooding evidence: Compono.Http body matching, Compono.Http header matching, and Compono.Logging structured verification. Admission (Gate B) is settled for all three; design readiness is not uniform — Compono.Logging's structured-verification gap can go straight to a Proposed ADR (its underlying capture representation already has everything the vocabulary needs), while Compono.Http's body/header matching needs a short, focused design spike first, because extending its matcher model touches a real architectural constraint (HttpResponseRegistrationBuilder's matcher is fixed at construction today) that the original research flagged but didn't resolve. Whether body and header matching end up as one fluent model or two separate mechanisms is exactly the kind of question that spike should settle — not pre-decided here. The corrected cosmere-tracker inspection added real, independent evidence for the Options-hand-wiring pattern Compono.Options already exists to fix (informative, not admission- relevant — Options verification specifically found no new supporting evidence). Everything else from the original pass (TestDoubles closed, async/disposal/binder deferrals unchanged) is untouched by this revision.
| Candidate | Verdict |
|---|---|
1. Compono.Http body matching | Admitted (product-owner decision) — needs a short design spike before ADR |
2. Compono.Http header matching | Admitted (product-owner decision) — same design spike as #1, boundary TBD by that spike |
3. Compono.Logging structured verification (WithProperty/WithMessageTemplate) | Admitted (product-owner decision) — ADR-ready now |
4. Compono.Options follow-ups (incl. verification) | Already adequately supported — no action; corrected cosmere-tracker evidence doesn't change this |
5. Profile/Compose<TProfile,TConfig> ergonomics | Documentation/skill improvement only — concrete changes identified below, plus a designed skill eval scenario |
| 6. TestDoubles remaining gaps | Already adequately supported — the two real 1.1 gaps (received-call records, AtLeast/AtMost) shipped in PR #134 |
| 7. Diagnostics/failure experience | Doc/skill finding stands; speculative detector stays research-only, deliberately not prioritized over 1–3 |
| Async composition | Deferred, unchanged |
| Disposal/lifetime ownership | Deferred, unchanged |
| Binder/framework consolidation | Deferred, unchanged (spike still unexecuted, still non-blocking) |
2. Current post-1.2 product surface¶
Eleven installable packages plus the embedded, non-packable Compono.Generators (docs/roadmap/future-packages.md): Compono, Compono.XunitV3, Compono.NSubstitute, Compono.Bogus, Compono.TUnit, Compono.TestDoubles, Compono.DependencyInjection, Compono.Http, Compono.Logging, Compono.MSTest, Compono.NUnit, and now Compono.Options (shipped v1.2.0, PR #135, ADR-0061). Since 1.1's three parallel research passes (RESEARCH-0023/0024/0025) and the CallVerifier/ClearCalls investigations (RESEARCH-0026/0027), the following shipped, confirmed directly against current source and git log:
CallVerifier.AtLeast(int)/AtMost(int),Compono.TestDoubles'ReceivedCalls(),ClearCalls()— PR #134 (RESEARCH-0025/0026/0027's top recommendations, in full).Compono.Logging'sLogVerificationBuilder.AtLeast/AtMostcompanion forwarders — present in current source (confirmed by direct read), closing RESEARCH-0027 §3.3's flagged companion gap.Compono.Options—TestOptionsSource<T>/UseOptions<T>(), shipped and dogfooded againstalexa-vox-craft'sMediatRTestProfile(ADR-0061, confirmed live inalexa-vox-craftcommit2cce6a6).Compono.Http'sRespondBytes(PR #133) — shipped; no async matching, header matching,WithJsonBody, orWhenAsyncexists in current source (confirmed by direct read ofsrc/Compono.Http/TestHttpHandler.csand grep for those names — zero matches). RESEARCH-0024's top recommendation is still entirely unbuilt.Compono.Logging'sWithProperty/WithMessageTemplate/matched-entries accessor — not built (confirmed by direct read ofLogVerificationBuilder.cs: onlyAtLevel/WithEventId/WithException<T>/WithMessageContaining/Matchingplus the count terminals exist). RESEARCH-0023's top recommendation is unbuilt.- A generator/Roslyn-version compatibility bug fixed in PR #136 (
a6d4a11, 2026-09-08) —Compono.Generators.dllwas built against a newer Roslyn (5.9.0) than the .NET 8/9/10 stable SDKs bundle, so the generator silently never ran (CS9057, warning-only), surfacing later as a misleading runtime "no generated plan"CompositionException. Real incident, not hypothetical — see Candidate 7.
3. Prior research/ADR findings incorporated¶
Directly read, not summarized from memory:
- RESEARCH-0023 (Logging 1.1) —
WithProperty/WithMessageTemplateranked #1, matched-entries accessor #2,AtLeast/AtMost#3 (now shipped). #1/#2 remain unbuilt and are re-evaluated below (Candidate 3). - RESEARCH-0024 (Http 1.1) — async body/header matching ranked #1,
RespondStreamexplicitly rejected, header matching alone ranked secondary/bundle-only. Fully re-evaluated below (Candidates ½) against fresh dogfooding evidence RESEARCH-0024 itself flagged as still needed (§18: "worth a light dogfooding pass before finalizing an ADR"). - RESEARCH-0025/0026/0027 (TestDoubles 1.1 +
CallVerifier) — both ranked recommendations shipped in PR #134. Re-verified against current source (Candidate 6) rather than assumed closed. - RESEARCH-0028 (Options admission) —
Compono.Optionsshipped per its recommendation;Compono.Configurationcorrectly stayed documentation-only (Cookbook entries now exist, confirmed present). Re-evaluated for genuinely new post-ship evidence (Candidate 4), not re-litigated. - RESEARCH-0015 (disposal) / RESEARCH-0016 (async composition) — both landed on Outcome C (fully additive, post-1.0, no urgency). Checked for new evidence since (dogfood repos, git history) — none found; verdicts stand unchanged (see Deferred Areas).
- RESEARCH-0019 (binder/framework duplication spike scope) — scoped but, per its own file, still not executed. No new maintenance incident (a second drift bug of the negative-seed-guard shape) was found in
git logsince PLAN-0061 to force escalation. - ADR-0052 Finding B (nested
context.Resolve<T>()discovery) — confirmed still open (docs/roadmap/post-mvp.md), unrelated to any candidate below except as a standing constraint noted where relevant (Options' "no automatic composition" scope).
4. Dogfooding evidence (Revised 2026-09-11 — cosmere-tracker correction)¶
Read-only inspection of alexa-vox-craft (current main, 3245d95/2cce6a6) plus, per the corrected path, a fresh read-only inspection of cosmere-tracker at /Users/ncipollina/source/repos/ncipollina/cosmere-tracker (current main, 4d25e14). The original pass's "no longer exists" conclusion was a wrong-path error, not a real absence — corrected here, not repeated.
cosmere-tracker — pins a very old Compono prerelease; explains, rather than contradicts, the original findings. Directory.Packages.props pins Compono/Compono.XunitV3/Compono.NSubstitute/Compono.Bogus at 0.1.0-alpha.33 — a prerelease that predates Compono.Http, Compono.Logging, and Compono.Options entirely (none of the three existed at that version). This means:
- No
Compono.Http/TestHttpHandlerusage exists — not because a consumer chose against it, but because it didn't exist yet at the pinned version. Instead,test/Cosmere.Tracker.TestKit/Profiles/ClientTestProfile.cshand-wiresSubstitute.For<HttpMessageHandler>()(Compono.NSubstitute) plus a hand-rolledHttpMessageHandlerExtensions.ReturnsResponse(...)helper (test/Cosmere.Tracker.TestKit/Extensions/HttpMessageHandlerExtensions.cs) that takes an optionalFunc<HttpRequestMessage, bool>predicate — the same synchronous-only shapeCompono.Http's ownWhen(...)has today. No test in the repo uses that predicate parameter to inspect a request body at all (grepped directly) — weak, inconclusive evidence either way on body matching specifically, but real, independent confirmation that hand-rolling this shape (predicate-gated response configuration over a substitute handler) is a pattern this consumer reached for on their own, consistent withCompono.Http's own original admission rationale (ADR-0051/RESEARCH-0009). - No
Compono.Loggingusage —ILogger<T>dependencies are satisfied via plainSubstitute.For<ILogger<T>>()throughout; no structured-log capture or verification of any kind. Same explanation: the package didn't exist at the pinned version. No.Matching(...)usage was found anywhere in the repo. - Real, independent evidence for the exact friction
Compono.Optionsalready exists to fix.test/Cosmere.Tracker.Shared.Tests/TestKit/Profiles/PersistenceTestProfile.cs:
builder.Register<IOptions<DynamoDbOptions>>(() => Options.Create(new DynamoDbOptions
{
TableMaps = new Dictionary<string, DynamoDbTableMap> { ... },
}));
and, separately, two test files (Persistence/BatchRepositoryTests.cs, Persistence/MissingIndexConfigurationTests.cs) construct a second, independently-typed Options.Create(new DynamoDbOptions { ... }) value by hand, outside any profile, alongside manually-constructed CosmereTrackerRepository(Substitute.For<...>(), Substitute.For<...>(), options) — bypassing Compono composition entirely for those tests. This is real hand-wiring of exactly the shape Compono.Options's UseOptions<T>()/TestOptionsSource<T> was built to replace (per ADR-0061's own before/after in docs/packages/compono-options.md), but it is historical evidence (predates Compono.Options by version, not a live decision to avoid it) and does not touch Options verification specifically — nothing here asks whether an option was read, changed, or how many times. This does not change Candidate 4's verdict: it corroborates why Compono.Options itself was worth building (already acted on, already shipped), not that verification on top of it is needed. - Compose<TProfile, TConfig> usage is real and idiomatic — [Compose<PersistenceTestProfile>] across Cosmere.Tracker.Shared.Tests/Persistence/* (dozens of call sites) and [Compose<ClientTestProfile>] in Cosmere.Tracker.Api.Tests. Consistent with alexa-vox-craft's own usage — no gap found here either.
Net effect of the correction: a second real dogfood repo now informs this research, and its evidence is consistent with, not contradictory to, every 2026-09-09 verdict below — it explains the Compono.Http/ Compono.Logging silence as version-pinning rather than avoidance, and it adds a second, independent data point for Compono.Options's own already-accepted rationale without surfacing anything new for Options verification.
Http — real evidence directly on point for Candidate 1. test/AlexaVoxCraft.Smapi.Tests/Auth/SmapiDeveloperAccessTokenProviderTests.cs:69:
var registration = handler.When(req => req.Content is FormUrlEncodedContent)
.RespondJson(lwaResponse);
This is a live, shipped, non-hypothetical instance of the exact "synchronous matcher pushes the consumer toward a weaker assertion" pattern RESEARCH-0024 predicted analytically. The test wants to assert the request body is form-encoded with specific fields (it's testing an OAuth token exchange); instead it can only check the runtime type of req.Content (is FormUrlEncodedContent), because reading the actual field values requires await content.ReadAsStringAsync(), which When's synchronous Func<HttpRequestMessage, bool> cannot express without .GetAwaiter().GetResult(). This test currently cannot verify what was sent, only its content type — a real, shipped test that is measurably weaker than the author's own intent, not a constructed example.
No .Result/.GetAwaiter().GetResult() anti-pattern was found anywhere in alexa-vox-craft's own test code (searched directly) — the consumer did not fall into the sync-over-async trap RESEARCH-0024 warned about, but only because they settled for a weaker assertion instead. That is arguably worse evidence-quality-wise for "forces an anti-pattern" and better evidence for "forces a silently weaker test" — a distinct, still real, cost.
Header matching: no req.Headers inspection was found in any When(...) predicate across the repo — no direct evidence either way for Candidate 2 as a standalone gap.
Logging: exactly two files reference Compono.Logging (PerformanceLoggingBehaviorTests.cs, MediatRTestProfile.cs). No use of .Matching(...) with a structured-property predicate was found in either — too thin a sample to call this either confirmed or refuted friction.
Options: MediatRTestProfile.cs is the real, already-cited Compono.Options dogfood target from ADR-0061 itself — confirmed live and matching the package guide's documented before/after exactly. No new gap surfaced beyond what ADR-0061's own dogfooding pass already found and closed.
Profiles/Compose<TProfile, TConfig>: heavy real usage — dozens of [Theory, Compose<LambdaTestProfile>]/[Compose<SmapiHttpTestProfile>] call sites across AlexaVoxCraft.MediatR.Lambda.Tests and AlexaVoxCraft.Smapi.Tests. All idiomatic, no hand-constructed dependency-wiring workarounds found anywhere searched. This is consumer-authored, human-written code (not agent-generated), so it says nothing about agent discoverability — see Candidate 5.
TestDoubles: not separately re-dogfooded this pass; RESEARCH-0025's own standalone-viability experiments and PR #134's shipped scope already supersede further inspection here.
5. Candidate-by-candidate analysis¶
Candidate 1 — Compono.Http async request/body matching¶
Current capability (re-verified against TestHttpHandler.cs directly, not RESEARCH-0024's now-9-days-stale snapshot): OnGet/OnPost/etc. match method + exact-or-Match<string> path. When(Func<HttpRequestMessage, bool>) is the only whole-request escape hatch, and it is fully synchronous. Zero request-body matching exists in any form — RespondBytes(PR #133) only affects response bodies. No WithHeader, WithJsonBody, WhenAsync, or Match<T>-based content matcher exists. SendAsync is already async Task<HttpResponseMessage>-returning (TestHttpHandler.cs:145), so awaiting an async matcher during dispatch is mechanically straightforward — nothing about the current shape has changed since RESEARCH-0024's analysis.
What a consumer must do today to match a JSON/form/raw request body: drop to When(...) and either (a) check req.Content's type only (the real alexa-vox-craft pattern, §4 — a materially weaker assertion than intended), or (b) call .GetAwaiter().GetResult() inside the predicate (a documented deadlock-risk anti-pattern, per RESEARCH-0024 §8.2 and MS's own DI guidance naming this "at all costs" avoidable). No consumer in the one real dogfood repo checked chose (b) — they chose (a) instead, which is real evidence of friction, just a different flavor than RESEARCH-0024 originally hypothesized.
Is this a real Compono.Http gap, or ordinary HttpClient test code? A real gap, specific to this package's own architecture: TestHttpHandler already has an async dispatch path (SendAsync) and already owns request matching as its core responsibility (ADR-0051) — the synchronous-only When predicate is an artifact of the matcher vocabulary, not something inherent to testing HTTP clients in general (an ordinary hand-rolled HttpMessageHandler fake can trivially await inside its own SendAsync override; Compono.Http's own When API just doesn't expose that capability to the consumer).
Smallest coherent capability. RESEARCH-0024 §8.2 already surveyed the shape question (WhenAsync(Func<HttpRequestMessage, ValueTask<bool>>) vs. a body-specific WithJsonBody<T>(Func<T, bool>)) and deliberately left it open pending real evidence. The alexa-vox-craft finding (§4) sharpens this: the real friction observed is specifically body-content assertion, not an arbitrary async predicate — the consumer wanted to assert on deserialized/parsed content (form fields), not on raw bytes. This favors a body-specific accessor (WithFormBody/WithJsonBody<T>) over a bare WhenAsync escape hatch as the more load-bearing addition, though both remain legitimate design-pass questions, not settled here.
Architectural cost, unchanged from RESEARCH-0024's analysis (re-verified, not re-derived): additive only; no existing signature changes; SendAsync's dispatch loop gains an await only when an async matcher is registered; ordering semantics change from a synchronous linear scan to a sequentially-awaited one (must be documented, not just implemented); zero change to IsAotCompatible=true posture (no reflection needed for a raw body/form-field read; a WithJsonBody<T> convenience would need the same JsonSerializerOptions?/JsonTypeInfo<T> overload split RespondJson<T> already uses). No generator involvement. RespondStream remains correctly rejected (RESEARCH-0024 §8.1 — unrelated to this candidate, response-side only).
Gate A: 1. Compono-specific value — clears. Not "HttpClient testing is hard in general" but "this package's own matcher vocabulary stops at path/ method and pushes a real consumer toward a weaker assertion than intended" — a Compono.Http-specific ergonomics gap, confirmed by a real shipped test, not merely theorized. 2. Native ecosystem fit — clears. Func<HttpRequestMessage, ValueTask<bool>>- shaped matching and JsonSerializerOptions?/JsonTypeInfo<T> overloads both mirror vocabulary the package already uses elsewhere (RespondJson<T>); nothing invented. 3. Meaningful abstraction — clears. The current workaround is not "a few obvious lines," it's a real bug-shaped tradeoff (weaker assertion or deadlock risk) — exactly Gate A's bar for "more than a trivial extension method." 4. Architectural fit — clears. Builds entirely on TestHttpHandler's already-async SendAsync; no core-Compono change, no reflection, no generator. 5. Package-boundary justification — n/a (addition to an existing package, not a new one).
Gate B: dogfooding evidence now exists and is real (§4) — a live, shipped alexa-vox-craft test with a measurably weaker assertion than the author's evident intent, directly caused by this gap. This satisfies the "real dogfooding evidence" trigger RESEARCH-0024 §18 explicitly said was still missing before finalizing an ADR. Weighed against ADR-0029's four questions: observed frequency — one real site found, in one repo (not "several distinct places," a real limitation, see §4's note on cosmere-tracker's absence); was this ever intended to work? — no, never promised, not a bug; workaround cost — real and concrete (a type-only check standing in for a field-level assertion, shown as an actual before/after in §4, not hypothetical); principle alignment — no conflict, fully additive and reflection-free.
Classification (Revised 2026-09-11): Admitted. Gate A clears cleanly on its own merits (unchanged from the original pass); Gate B is now doubly satisfied — both by the real alexa-vox-craft dogfooding evidence above (independently sufficient on its own, per the original 2026-09-09 analysis) and, separately, by the explicit product-owner decision that admits this regardless of dogfooding friction. Not yet ADR-ready as-is — see §5a. The exact matcher shape (WithJsonBody<T> vs. WhenAsync vs. both, and whether it composes with header matching in one model) needs a short, focused design spike first, because it interacts with a real constructional constraint in HttpResponseRegistrationBuilder, not because admission itself is in doubt.
Candidate 2 — Compono.Http header matching¶
Re-verified: still zero WithHeader-shaped API; still only reachable via When(req => req.Headers.Contains(...)). No dogfood evidence found this pass (§4) — no alexa-vox-craft test inspects headers inside a When predicate at all. RESEARCH-0024 §8.3's own analysis is otherwise unchanged: synchronous, low complexity, but touches HttpResponseRegistrationBuilder's "matcher fixed at construction" shape non-trivially for what it buys (better diagnostics only — headers are already synchronously inspectable).
Gate A: plausibly clears on the same reasoning as Candidate 1 (a small slice of the same friction cluster) — RESEARCH-0024 itself only ever recommended this "if bundled with #1's design work; otherwise defer," written before admission was a settled question.
Gate B (Revised 2026-09-11): no dogfooding evidence this pass (the cosmere-tracker correction, §4, doesn't add any either — no test there inspects headers inside a predicate) — but Gate B is now satisfied independently by the explicit product-owner decision: "headers are a normal matching dimension if Compono.Http owns request matching," admitted without conditioning on dogfooding friction.
Classification (Revised 2026-09-11): Admitted, same footing as Candidate 1. Whether this ships as part of one coherent fluent request-matching model together with body matching, or as an independent, separately-shaped mechanism, is explicitly not decided by the product-owner's direction — that boundary is exactly what §5a's design spike should determine from the real architecture, not force one way or the other in advance. Real semantics this needs to cover once designed: request headers vs. content headers (HttpRequestMessage.Headers vs. HttpRequestMessage.Content?.Headers are two different collections in .NET's own model — a header matcher that only looks at one would be a real, silent gap for the other), multiple values per header name, case-insensitive header-name comparison (HTTP header names are case-insensitive by spec; .NET's own HttpHeaders already treats them that way), exact-vs-contains value semantics, a missing header (Authorization never set — is that "doesn't match" or an error?), and diagnostics that name the header on a no-match failure the same way OnGet(path) already names the path. None of these are resolved here — they are the concrete list a design pass needs to work through.
Candidate 3 — Compono.Logging structured verification¶
Re-verified against current LogVerificationBuilder.cs: AtLeast/ AtMost shipped (closing that part of RESEARCH-0023); WithProperty/ WithMessageTemplate/a matched-entries accessor did not ship. Every finding in RESEARCH-0023 §5/§6 about CapturedLogEntry already capturing MessageTemplate/Properties but Verify() only reaching them through .Matching(...) (with the failure-message quality loss — Describe() still renders "a custom condition" for any Matching(...) call, confirmed unchanged in current source) is unchanged and still accurate.
Dogfooding this pass: only two alexa-vox-craft files touch Compono.Logging, and neither exercises structured-property/template verification — too thin a sample to add or subtract confidence versus RESEARCH-0023's own honest admission (§6-#4) that its candidates were derived from reading the code, not from an observed complaint.
Gate A: clears on the same reasoning RESEARCH-0023 §8 already established (additive, no core-Compono/generator touch, directly closes a captured-but-unreachable-through-Verify() gap) — nothing here has changed to weaken that analysis.
Gate B (Revised 2026-09-11): the cosmere-tracker correction (§4) still adds no dogfooding evidence (the repo has no Compono.Logging usage at all, for a version-pinning reason, not a design one) — but Gate B is now satisfied independently: the product owner explicitly admitted this as "a natural verification vocabulary over what's already captured, not a general logging assertion framework," a direct echo of RESEARCH-0023's own framing.
Classification (Revised 2026-09-11): Admitted — ADR-ready now. Unlike the two Compono.Http candidates, this one needs no design spike first — see §5a: CapturedLogEntry's representation already has everything a WithProperty/WithMessageTemplate vocabulary needs, and the extension is a mechanical mirror of LogVerificationBuilder's existing filter pattern. Recommend proceeding straight to a Proposed ADR, scoped by the semantics questions worked through in §5a below.
5a. Design-readiness assessment for the three admitted capabilities (new, 2026-09-11)¶
Reasoned directly from current source (src/Compono.Http/, src/Compono.Logging/), not from aesthetics or the original ADRs' prose alone.
Compono.Http request matching — needs a short design spike first¶
Current architecture, read directly: TestHttpHandler.OnGet(path)/ .OnPost(path)/etc. and .When(predicate) each construct a HttpResponseRegistrationBuilder whose constructor takes a finished Func<HttpRequestMessage, bool> matcher and immediately builds the backing HttpResponseRegistration(matcher, description) — the matcher is committed at the moment the builder is created, before any Respond*/Throws terminal call (src/Compono.Http/HttpResponseRegistrationBuilder.cs:22-26, src/Compono.Http/HttpResponseRegistration.cs:10-21). There is no mutable, accumulating matcher state on the builder today — On(method, path) and When(predicate) are two independent private factories in TestHttpHandler.cs that each produce one final predicate directly. TestHttpHandler.SendAsync's own dispatch loop (TestHttpHandler.cs:156-164) is a plain synchronous for loop calling _registrations[i].Matches(request) — HttpResponseRegistration.Matches (HttpResponseRegistration.cs:24) is a direct synchronous delegate invocation, not awaited, because nothing about it is async today.
What this means concretely for the three admitted capabilities:
- Fluent composition (
OnPost(path).WithHeader(...).WithJsonBody<T>(...)) requiresHttpResponseRegistrationBuilderto hold an accumulating matcher — a real, if bounded, internal shape change (a mutable composed-predicate field, or a smallList<Matcher>-ANDed-together design) — not a drop-in addition. This is exactly the "matcher fixed at construction" limitation RESEARCH-0024 §8.3 already named for header matching alone; it applies identically, and with higher stakes, once body matching is added to the same builder. - Async matching forces a real dispatch-model decision:
SendAsync's loop currently never awaits anything mid-scan. Any body matcher that readsrequest.Contentneedsawait, which means either (a)HttpResponseRegistration's matcher becomes uniformlyFunc<HttpRequestMessage, ValueTask<bool>>internally (with every existing sync matcher — path/method/header — trivially wrapped in an already-completedValueTask<bool>, costing nothing extra for the fully-sync case), or (b) two parallel matcher fields/paths coexist (sync-only fast path plus a separate async path), adding real internal branching and a second code path to keep consistent. This research does not pick between (a)/(b) — that is exactly the design-spike question — but confirms both are mechanically buildable on top ofSendAsync's already-async Task<HttpResponseMessage>-returning signature, and that today's code gives no free answer either way. - Precedence semantics must not silently change.
SendAsync's last-registered-first, first-match-wins scan (TestHttpHandler.cs:156-164) is a real, documented contract (TestHttpHandler's own XML doc, "Matching"). Introducing anyawaitinto that scan changes it from an instantaneous synchronous walk to a sequentially-awaited one — behaviorally identical in outcome (still evaluates in the same order, still stops at the first match) but different in execution shape, and must be documented as such, exactly as RESEARCH-0024 §13 already flagged. Finish()'s one-shot invariant (HttpResponseRegistrationBuilder.cs:129-147, the_finishedguard preventing a builder from being finalized twice) is unaffected by adding matcher-accumulation methods before the terminalRespond*/Throwscall — this invariant only guards the terminal step, not matcher composition, so it constrains but doesn't block the design.
Should body helpers be built on an internal-only WhenAsync primitive, or should WhenAsync also be public? Both are legitimate outcomes of the same underlying dispatch-model decision (point 2, above) — an internal async-matcher abstraction that WithJsonBody<T>/WithHeader compose on top of is architecturally necessary either way; whether that same primitive is also exposed publicly as a general escape hatch (mirroring When's existing role as the sync escape hatch) is a narrower API-surface question the design spike should answer by weighing it against Compono.Http's stated non-goal of becoming a general request-predicate DSL (RESEARCH-0024 §15's rejected "general-purpose HTTP mocking framework features").
Should body and header matching be one design effort? They share the identical underlying architectural blocker (matcher composability at the builder level) and the identical dispatch-model question (does the matcher become async-uniform), so doing them in one design pass is clearly more coherent than two separate ones — but that does not mean they must ship as one capability or one API surface. The design spike may reasonably conclude header matching is purely synchronous sugar layered on the same composable-matcher mechanism the async body work needs anyway, while body matching is the piece that actually needs the async dispatch change — in which case they'd share an internal mechanism but expose genuinely different public shapes (e.g., WithHeader staying fully synchronous while WithJsonBody<T> is the only async-capable addition). This research does not force them together or apart; it confirms only that evaluating them together, in the same design pass, is the efficient and coherent way to resolve the shared architectural question once rather than twice.
Verdict: needs a short, focused design spike before an ADR — not because admission is unclear (it isn't, per the product-owner decision), but because the specific shape of the matcher-composition and sync/async-dispatch decisions is a real fork in the current architecture that a Proposed ADR should walk in with already resolved, the same way ADR-0051's own original design pass resolved "reuse Match<T> or stay HTTP-native" before locking anything.
Compono.Logging structured verification — ADR-ready now¶
Current representation, read directly: CapturedLogEntry (src/Compono.Logging/CapturedLogEntry.cs) already exposes Properties (IReadOnlyList<KeyValuePair<string, object?>>?) and MessageTemplate (string?, Properties's "{OriginalFormat}" entry already extracted and surfaced by name — LogEntryCollector.ExtractStructuredState, confirmed directly: {OriginalFormat} is filtered out of Properties and returned separately as MessageTemplate, so a consumer never needs to special-case it). Scopes is already a distinct list, structurally separate from Properties — the representation already keeps "per-entry structured data" and "ambient scope data" apart, exactly the distinction a WithProperty filter needs to respect (operate on Properties only, leave Scopes for a possible, separately-named future filter, not conflated).
No internal representation change is needed. Every piece of information the admitted capability needs is already captured, typed, and public on CapturedLogEntry today. The gap is entirely in LogVerificationBuilder's filter vocabulary (src/Compono.Logging/LogVerificationBuilder.cs), which already has a uniform, trivially-extensible shape: every existing filter (AtLevel, WithEventId, WithException<T>, WithMessageContaining) is a one-line call to the same private Add(string description, Func<CapturedLogEntry, bool> predicate) helper (LogVerificationBuilder.cs:70-74), and every filter composes via simple AND-together evaluation in ToCallVerifier() (LogVerificationBuilder.cs:76-97) regardless of how many are chained — including the already-shipped AtLeast/AtMost/Exactly/Once/Never terminals, which read the same filtered match count unconditionally. A WithProperty/WithMessageTemplate addition is mechanically identical to every filter that already exists:
public LogVerificationBuilder WithMessageTemplate(string template) =>
Add($"message template \"{template}\"", entry => entry.MessageTemplate == template);
public LogVerificationBuilder WithProperty(string name, object? value) =>
Add($"property \"{name}\" = {value}",
entry => entry.Properties?.Any(p => p.Key == name && Equals(p.Value, value)) == true);
(Illustrative — the exact semantics below are the ADR's decision to make, not settled here.)
Real semantics an ADR needs to settle, all answerable from the existing representation with no further research:
- Property value comparison —
Equals(p.Value, value)(the staticobject.Equals, null-safe both directions) is the natural default, mirroringWithEventId's existing==equality-only filter; no structural/deep-equality machinery is implied unless a future ADR wants it. - Null values —
Properties' value slot is already nullable by design (ADR-0055's own "Properties nullability" decision, confirmed inCapturedLogEntry.cs's own doc comment);object.Equals(null, null)istrueandobject.Equals(null, "x")isfalse, so no special-casing is needed beyond using the null-safe comparison already chosen above. - Duplicate property names —
Propertiesis a plainIReadOnlyList<KeyValuePair<...>>, which can legitimately contain the same key more than once (e.g. a scope contributing the same name as the message state)..Any(p => p.Key == name && ...)naturally means "at least one entry with this name has this value" — the same any-match-suffices semanticsMatching(...)already has today for an equivalent hand-written predicate. No new decision needed unless a future ADR wants an exact-count variant, which isn't implied by anything captured evidence points to. {OriginalFormat}— already fully handled: it never appears inProperties(filtered out at capture time), soWithPropertynever needs to special-case it, andWithMessageTemplateis the correct, already-separated way to assert on it.- Template identity vs. formatted message — already two distinct, already-captured fields (
MessageTemplatevs.Message), withWithMessageContaining/a futureWithMessageTemplatecleanly mapped to each — no ambiguity to resolve. - Destructuring (
@Propertysyntax) —ExtractStructuredStateextracts whateverTState's ownIReadOnlyList<KeyValuePair<string, object?>>already contains, which already includes the destructured object asp.Value(an ordinary boxed value in the list, same as any other logged argument) — no special handling needed;WithPropertycompares whatever value the framework's own logging call already put in that slot, destructured or not. - Scopes vs. entry properties —
Scopesis a separate list onCapturedLogEntry;WithPropertyoperating only onPropertiesis consistent with every other existing filter (none of which inspectScopestoday). A futureWithScope/InScopefilter is a distinct, separately-evidenced question, not implied or blocked by this one. - Interaction with
AtLevel/WithEventId/WithException<T>/WithMessageContaining/Matching/AtLeast/AtMost— automatic and free: every filter method returnsthisand appends to the same_filterslist, ANDed together uniformly inToCallVerifier()regardless of which filters are present or their order;AtLeast/AtMost(already shipped) read the same filtered count exactly likeOnce/Never/Exactlydo. No interaction risk exists to resolve. - Diagnostic quality —
Describe()(LogVerificationBuilder.cs:99-113) already concatenates each filter's own description string with" and ";WithProperty/WithMessageTemplatesimply need descriptive strings following the same pattern shown above (e.g.property "OrderId" = 42), directly closing the "a custom condition" diagnostic-quality lossMatching(...)currently produces for the same assertion.
Verdict: ADR-ready now. Every question a design pass would normally need a spike to answer is already resolvable directly from the existing, unchanged representation and the existing, uniform filter-composition mechanism — there is no architectural fork here comparable to Compono.Http's matcher-construction/async-dispatch questions.
Candidate 4 — Compono.Options follow-up capabilities¶
Reviewed the shipped public surface (src/Compono.Options/: CompositionBuilderExtensions.cs, FrozenOptionsView.cs, TestOptionsSource.cs, UnconfiguredNamedOptionException.cs) and the package guide directly (§2 above). No test, doc, skill-eval, or dogfooding evidence surfaced any limitation — the one real dogfood consumer on a current Compono version (alexa-vox-craft) uses exactly the documented before/after pattern with no workaround, no escape to hand-rolled fakes, and no unmet need.
Corrected cosmere-tracker inspection (§4) checked explicitly, per product-owner instruction, for evidence that would change this verdict — found none for verification specifically. It does show real, independent hand-wiring of Options.Create(new DynamoDbOptions {...}) outside any profile in two test files, but that repo pins a Compono.Options-less prerelease (0.1.0-alpha.33) — the finding corroborates why UseOptions<T>()/TestOptionsSource<T> were worth building (a real pattern that predates and motivates the package), not that verification on top of the now-shipped package is needed. No test anywhere in either dogfood repo asks whether an option was read, changed, or how many times — the specific capability the owner declined to request.
Re-examined the specifically-named candidate — Options verification (was an option read, did a change occur, how many times, subscriber behavior): no evidence this is needed. Compono.Options's entire design intent (per ADR-0061/RESEARCH-0028) is to be a coherent source of truth, not an assertion surface — CallVerifier/Verify() belongs to Compono.TestDoubles/Compono.Http/Compono.Logging's "was a member called" model, which has no analog here (a settings value isn't "called"). Inventing an Options-specific verification vocabulary with no evidence would be exactly the kind of unevidenced generality docs/architecture/capability-admission.md Step ⅔ exists to catch — this is the honest place to say "no feature needed," not a gap in disguise.
Named-option initialization and profile/context-aware composition (§12a of RESEARCH-0028, re-confirmed live in alexa-vox-craft's MediatRTestProfile) both already work exactly as documented — no ergonomic gap found.
Classification: Already adequately supported. No action. This is a genuine "don't build ahead of demand" outcome, not an oversight — Compono.Options shipped nine days before this research and already has one real, clean dogfood validation with zero friction found.
Candidate 5 — Profile/context-aware composition ergonomics (Revised 2026-09-11)¶
ICompositionProfile.Configure(CompositionBuilder) remains a plain, unrestricted sequence of ordinary builder calls (confirmed directly against src/Compono/CompositionBuilder.cs/ICompositionProfile.cs, same as RESEARCH-0028 §12a already established). Compose<TProfile, TConfig> is real, shipped, documented (docs/packages/compono-xunitv3.md's "Profile configuration arguments" section), and heavily used correctly in real, human-authored alexa-vox-craft and cosmere-tracker code (§4) — dozens of clean call sites across both, zero hand-constructed dependency-wiring workarounds found anywhere searched in either.
No evidence of an API-level gap: Register<T>, Share<T>(), Compono.Options's UseOptions<T>(), and profile composition all already read naturally both inline and inside a profile (confirmed directly, not assumed, matching RESEARCH-0028 §12a's own experiment). The owner agrees with this — no new runtime API is being requested. The concrete input this revision responds to is different in kind from what a human- authored-code search can surface: a coding agent, given a real task (a SUT depending on IOptions<MyOptions>, test-specific option values needed), tried to manually construct/wrap the Options dependency instead of recognizing "this is exactly what a context-aware Profile plus Compose<TProfile, TConfig> plus Compono.Options is for." This is discoverability evidence, not an API gap — consistent with the task's own instruction that an agent's difficulty finding a pattern is useful DX signal but not proof a runtime API is missing.
Where the discoverability gap actually lives, read directly rather than assumed: skills/compono/references/registrations-profiles-and-scopes.md documents ICompositionProfile (§"ICompositionProfile") and [Compose<TProfile, TConfig>] (§"Share<T>()" and its own "Don't confuse this with [Compose<TProfile, TConfig>]'s profile-configuration-argument mechanism" caveat) — but never once mentions Compono.Options, UseOptions<T>(), or TestOptionsSource<T> (confirmed by direct read and grep). skills/compono/references/options.md documents Compono.Options's own usage thoroughly, including an inline-and-profile example (mirroring docs/packages/compono-options.md's own worked example) — but that example never uses Compose<TProfile, TConfig> specifically to vary the profile per test/context; it shows a profile composing a fixed value, not a profile selected or parameterized per test via TConfig. Neither reference file is wrong, but the specific combination the agent needed — "test-specific config that should drive which/how a Profile builds the graph → reach for Compose<TProfile, TConfig> over an Options dependency, not hand-construction" — is not stated as a named, cross-referenced pattern in either file. This is a precise, fixable documentation gap, not a vague "add more examples" finding.
Recommended concrete doc/skill changes (not applied — description only, per this task's research-only scope):
skills/compono/references/registrations-profiles-and-scopes.md— add a cross-reference from theICompositionProfile/[Compose<TProfile, TConfig>]sections tooptions.md, naming the specific trigger explicitly: "When test-specific configuration should influence how the graph is built (not just which value one dependency gets), prefer a context-awareProfileselected viaCompose<TProfile, TConfig>over hand-constructing the SUT's dependencies — seeoptions.md'sCompono.Options-in-a-profile example for the commonIOptions<T>-shaped case of this."skills/compono/references/options.md— extend its existing inline-and-profile example (§"Inline and profile usage") to show the context-varies-per-test shape specifically: aTConfig-parameterized profile that changes whichTestOptionsSource<T>value it registers based on theTConfigargument, with[Compose<TProfile, TConfig>(arg)]selecting it per test — not just "a profile can hold a fixedUseOptions<T>()call," which is all the current example shows.SKILL.md's own top-level routing table — confirm (not found missing, but worth an explicit check during implementation of the above) that a request shaped like "this test needs different option values per test case" routes a reader/agent toregistrations-profiles-and-scopes.mdoptions.mdtogether, not to either alone.
These three are documentation-only, additive, and require no API, compile-time, or runtime change — exactly matching the owner's own framing ("doc/skill work, not API work, unless you find a genuine API limitation"). No genuine API limitation was found.
Designed (not added) skill eval scenario, per the owner's request:
- Scenario: a SUT constructor depends on
IOptions<MyOptions>. The test suite has (or should have) a reusableICompositionProfilefor this SUT's area, and the specific option values needed vary by test context (e.g., a "valid configuration" case and an "invalid/missing configuration" case, or a per-environment value). - Prompt shape: ask the agent to write a test (or several) for this SUT where two or more tests need different
MyOptionsvalues, framed the way a real ticket would be framed (not naming Compono APIs directly) — e.g. "add a test verifying the service behaves correctly when the configured retry count is zero, alongside the existing happy-path test." - Expected solution shape: a
TConfig-parameterizedICompositionProfilethat callsUseOptions<T>(new TestOptionsSource<MyOptions>(...))(or an equivalentRegister<IOptions<MyOptions>>/Compono.Optionscall) with a value derived fromTConfig, selected per test via[Compose<TProfile, TConfig>(config)]— matching the exact pattern named in the product-owner's own framing. - Failure signature to detect (this is the concrete, undesired behavior the eval should catch): the agent hand-constructs the SUT directly (
new MySut(Substitute.For<IDep>(), Options.Create(new MyOptions {...}))) instead of composing it, or hand-wiresOptions.Create(...)inline in each test rather than through a reusable, context-aware profile — either pattern should score as a miss even if the resulting test technically passes, since the point of the eval is discoverability of the composition-native path, not test correctness alone. - Not implemented in this pass — this is a description of the scenario and its scoring signal for whoever adds it to the skill's eval suite next (mirroring the existing
eval-28/eval-29/eval-30/eval-31pairs' own before/after structure), not a new eval file.
Gate A/B: no candidate to evaluate as a capability — nothing surfaced meets Step 1's "a real, concrete problem" bar for new API. The real finding is entirely a documentation/discoverability gap, addressed concretely above.
Classification: Documentation/skill improvement only. No ADR warranted. The three doc/skill changes above and the designed eval scenario are the complete, concrete output of this candidate.
Candidate 6 — TestDoubles remaining gaps¶
Every gap RESEARCH-0025/0026/0027 identified as real and worth shipping — received-call records (ReceivedCalls()), ClearCalls(), and CallVerifier.AtLeast/AtMost — shipped in PR #134, confirmed directly against current src/Compono/CallVerifier.cs and the package's own docs/packages/compono-testdoubles.md. This closes what was, at the time of RESEARCH-0025, explicitly named as "the single next missing primitive."
Remaining named-but-rejected items from RESEARCH-0025 (call-order verification, strict mode/partial substitutes, ref/out/in support, cancellation-aware auto-throw defaults, standalone-package independence) were all explicitly evaluated and rejected there on architectural-fit or evidence grounds specific to this package staying "a fallback default- value generator for otherwise-unresolvable composition-graph leaves," not a general-purpose mocking framework (ADR-0042). No new evidence surfaced this pass to revisit any of them — asking, for each, "is this something Compono's source-generated test-double model should naturally support, or a full-mocking-framework feature Compono intentionally isn't becoming?" still lands on the latter for every one of them, exactly as RESEARCH-0025 concluded.
No new gap was surfaced by this pass's own dogfood check (not separately re-run — RESEARCH-0025's own three real experiments already settled the standalone-viability question decisively, and no code change since would alter that).
Classification: Already adequately supported. No action. The package correctly stopped where its own governing ADR says it should.
Candidate 7 — Diagnostics and failure experience¶
The concrete, fresh incident: PR #136 (a6d4a11, 2026-09-08, three days before this research) — Compono.Generators.dll was built against Microsoft.CodeAnalysis.CSharp 5.9.0, newer than the Roslyn versions bundled with the stable .NET 8/9/10 SDKs. Roslyn's own analyzer-loading gate silently refuses to load an analyzer built against a newer compiler, emitting only a build warning (CS9057), never an error — so the generator simply never ran on any of those SDKs, and the first signal a consumer saw was a runtime CompositionException reading "no generated plan," with no evident connection to a compiler-version mismatch. Neither this repo's own CI nor either then-current dogfood consumer caught it, because all three pin a preview SDK.
This is exactly the failure shape the task named as evidence worth weighing: a generator can fail during compilation and later manifest as an apparently unrelated runtime "no generated plan" failure — not a hypothetical, a real, dated, fixed incident.
Distinguishing the diagnostic layers, per the task's own framing:
- Generator/compiler diagnostics — the real gap.
CS9057is Roslyn's own warning, easy to miss in ordinary build output, and nothing in Compono's own generator surfaces a Compono-specific, harder-to-miss signal (aCMPxxxxdiagnostic, or a doc callout) connecting "generator didn't run" to "you'll get a runtimeCompositionException, not a build error." The fix that shipped (flooring the package's ownMicrosoft.CodeAnalysis.CSharpreference) prevents this specific version-skew case from recurring, but doesn't generally close the gap that a differently-caused generator non-execution (a consumer's own unusual toolchain, a future SDK bump on Compono's side reintroducing the same class of issue) would still fail silently the same way. - Runtime diagnostics —
CompositionException's "no generated plan" message is accurate but doesn't distinguish "this type was never meant to be composed" from "the generator should have run for this type but didn't" — the second case is exactly what this incident hit, and a consumer has no way to tell which one they're looking at from the exception alone. - Documentation/troubleshooting — no existing troubleshooting doc entry connects "generator didn't run" symptoms (a runtime "no generated plan" failure despite code that looks correct) to "check for a
CS9057warning" as a first diagnostic step. This is the cheapest, most directly actionable part of this candidate. - Capability/API change — none identified as necessary; this is not a case calling for new machinery, "significant complexity," or a runtime reflection fallback (correctly out of scope per the task's own instruction not to propose machinery to paper over unsupported environments).
Gate A: a documentation/troubleshooting addition (connecting CS9057 to a runtime "no generated plan" symptom) clears trivially — it's exactly the kind of "a useful compile-time diagnostic is better than a clever runtime fallback" principle this repo already holds (CLAUDE.md's core philosophy), applied to making an existing Roslyn diagnostic discoverable rather than inventing a new mechanism. A new, Compono-owned diagnostic that detects "the generator apparently didn't run for a composable type reachable in this compilation" is a more substantial, real design question — plausible in principle (the generator could, in theory, emit a warning if it detects zero composable types were ever processed in an otherwise-Compono-referencing compilation) but not analyzed in enough depth here to call it Gate-A-clear on its own; it needs its own focused investigation into false-positive risk (a compilation that legitimately has zero composable types today is not an error).
Gate B: one real, dated, concrete incident — satisfies the evidence bar for at least the documentation half of this candidate immediately.
Classification: Worth further focused research for the diagnostic-API half (does a "generator silently didn't run" detector make sense, and can it avoid false positives on legitimately generator-free compilations); documentation/skill improvement only, and should happen regardless of that outcome, for the troubleshooting-doc half (connect CS9057 to the runtime symptom in docs/reference/diagnostics.md or a troubleshooting page, and in the compono skill's own reference material) — this part needs no design pass and could ship immediately as a docs-only change.
6. Newly discovered candidates¶
No new capability-shaped candidate was found beyond the seven named areas. The one genuinely new item surfaced is Candidate 7's diagnostic gap, which the task's own framing already anticipated by name (the generator/Roslyn incident) rather than this research discovering it independently — recorded above, not double-counted here.
A broader search (TODOs, roadmap items, skill troubleshooting guidance) turned up nothing new: docs/roadmap/post-mvp.md's two outstanding roadmap candidates (ADR-0052 Part A/Finding B, the nested-Resolve<T>() discovery gap) are unrelated to this research's scope (compile-time composition discovery, not a testing-package feature) and already have their own tracked, Proposed status — not re-litigated here. docs/roadmap/future-packages.md lists no admitted candidates and no roadmap items beyond Compono.Options, which has now shipped.
7. Deferred areas — re-checked, not re-litigated¶
Async composition (RESEARCH-0016, Outcome C). No new evidence found — alexa-vox-craft still shows no async-construction-required pattern (a fresh grep for Testcontainers/LocalStack/ConnectAsync-shaped composition-time needs found the same nothing RESEARCH-0016 found). The deferral remains correct; no action.
Disposal/lifetime ownership (RESEARCH-0015, Outcome C). No new disposable-composed-value pattern found in alexa-vox-craft beyond the already-recorded HttpTestHarness case RESEARCH-0015 already evaluated. dynamodb-distributed-lock's Meter finding was not re-checked (out of scope for this Compono-focused pass) but nothing here contradicts it. The deferral remains correct; no action.
Binder/framework consolidation (RESEARCH-0019). The spike remains scoped but unexecuted — confirmed the file's own status is unchanged. git log since PLAN-0061 shows no second drift-bug incident of the negative-seed-guard shape that would force escalation per the spike's own "Escalation to a correctness/public-API concern" section. Remains correctly non-blocking and unprioritized; no action needed to execute it now, though it remains available to pick up opportunistically.
8. Ranked surviving candidates (Revised 2026-09-11)¶
Three candidates are now admitted; ranking reflects design readiness, not admission strength (all three are equally admitted) — per the product owner's own instruction that admission and API-design/ implementation-planning are distinct questions.
1. Compono.Logging structured verification (Candidate 3) — ADR-ready, highest near-term priority¶
- Concrete consumer problem:
WithProperty/WithMessageTemplatefilters don't exist; a consumer asserting a structured property value must drop to.Matching(...), losing named, descriptive failure messages. - Evidence: admitted by explicit product-owner decision (§ Revision); code-level gap independently confirmed (RESEARCH-0023, re-verified above). No dogfooding incident in either repo checked — not required, since admission doesn't depend on it.
- Recommended home:
Compono.Logging(existing package, additive). - Smallest plausible capability: two new filter methods on
LogVerificationBuilder, matching the existingAtLevel/WithEventIdpattern exactly — see §5a for the concrete semantics already resolved. - Implementation complexity: low.
- Compatibility/AOT: clean, purely additive, no reflection.
- ADR required: yes.
- Further research needed first: none — §5a resolves every semantics question (comparison, nulls, duplicates,
{OriginalFormat}, destructuring, scopes-vs-properties, terminal interaction, diagnostics) directly from the existing representation. - Priority: High, and the fastest of the three to actually ship — no design spike blocks it.
2. Compono.Http body + header request matching (Candidates 1 and 2) — admitted, needs a design spike first¶
- Concrete consumer problem: a request-matching test in a real dogfood consumer settles for a type-only check (
req.Content is FormUrlEncodedContent) instead of the field-level assertion its own test name implies, because the only escape hatch (When) is synchronous and body reads are async; headers face the same "only viaWhen, no first-class vocabulary" gap, independent of any observed friction. - Evidence:
alexa-vox-craft,test/AlexaVoxCraft.Smapi.Tests/Auth/SmapiDeveloperAccessTokenProviderTests.cs:69(§4),cosmere-tracker's own hand-rolledReturnsResponse(...)predicate parameter (§4), plus explicit product-owner admission for both body and header matching independent of dogfooding evidence. - Recommended home:
Compono.Http(existing package, additive). - Smallest plausible capability: not decided — explicitly left open by the product owner and by §5a's design-spike recommendation. The spike should determine whether body and header matching share one fluent model or ship as separately-shaped mechanisms sharing an internal matcher abstraction.
- Public API impact: additive only; no existing signature changes.
- Implementation complexity: moderate-to-real — §5a identifies a genuine architectural fork (
HttpResponseRegistrationBuilder's matcher fixed at construction; whether the internal matcher becomes uniformly async-capable or keeps two parallel sync/async paths) that a design pass must resolve before an ADR can lock a shape. - Compatibility/AOT: clean in principle — no reflection required for raw/form/header content; a JSON convenience needs the same
JsonSerializerOptions?/JsonTypeInfo<T>splitRespondJson<T>already uses.IsAotCompatible=trueposture preserved regardless of the chosen shape. - ADR required: yes, after the spike below.
- Further research needed first: yes — a short, focused design-dive (
tasks/design.md), scoped exactly to §5a's open questions: the matcher-composition mechanism onHttpResponseRegistrationBuilder, the sync/async dispatch-model choice inTestHttpHandler.SendAsync, and whether an internal (or also-public)WhenAsync-shaped primitive is the right foundation for both body and header helpers. - Priority: High for the design spike; the ADR itself follows once that spike resolves the architectural fork. Not blocked on more evidence — blocked on a design decision this research correctly declined to make for the requester.
3. Diagnostics: generator-silent-failure troubleshooting doc (Candidate 7, doc half)¶
- Concrete consumer problem: a generator that silently fails to run (for any reason, not just the one root cause PR #136 fixed) surfaces only as an unrelated-looking runtime
CompositionException, with nothing connecting the two. - Evidence: PR #136 (
a6d4a11), a real, dated, fixed incident. - Recommended home:
docs/reference/diagnostics.mdand/or a troubleshooting page, plus thecomponoskill's reference material. - Smallest plausible capability: documentation only — no code change.
- ADR required: no.
- Priority: Medium, but zero-cost — can ship immediately as a docs-only PR independent of anything else in this research.
4. Diagnostics: generator-non-execution detector (Candidate 7, API half)¶
- Concrete consumer problem: same incident, but asking whether Compono itself could detect and warn about the general case (any cause of the generator silently not running), not just document the one root cause already fixed.
- Evidence: same one incident; no second occurrence found.
- Recommended home:
Compono.Generators, if pursued. - Smallest plausible capability: unresolved — needs its own investigation into false-positive risk before any design is proposed.
- ADR required: not yet — needs focused research first.
- Priority: Low. Real, but speculative until a design-worthy shape is found; the doc-only half (#3) already captures nearly all of this incident's practical value at near-zero cost.
5. Profile/Compose<TProfile, TConfig> doc/skill changes (Candidate 5)¶
- Concrete problem: a coding agent, given a real task, hand-constructed an Options-dependent SUT instead of recognizing the context-aware Profile pattern — a discoverability gap, not an API gap (owner agrees).
- Evidence: the product owner's own observed agent behavior; the precise documentation gap independently confirmed above (§ Candidate 5 revised:
registrations-profiles-and-scopes.mdnever mentionsCompono.Options, andoptions.md's own profile example never shows the context-varies-per-test shape). - Recommended home:
skills/compono/references/registrations-profiles-and-scopes.mdandoptions.md(cross-reference + extended example), plus a new skill eval scenario (designed, not added — see above). - Smallest plausible capability: documentation only.
- ADR required: no.
- Priority: Medium, ship independently of the other four — cheap, concrete, and doesn't wait on any design spike.
9. Explicit recommendation: does Compono need another capability¶
immediately after 1.2? (Revised 2026-09-11)
Yes for admission — three capabilities are now explicitly admitted (Compono.Http body matching, Compono.Http header matching, Compono.Logging structured verification) — but this is not a signal to lump them into one forced release or to treat their APIs as settled. Admission (a product decision, now made) is distinct from design readiness (uneven across the three — Logging is ADR-ready, Http needs a spike first) and from implementation planning (not started for any). The practical sequencing this research recommends: ship the documentation-only items first (the profile/Options doc changes, §8 item 5, and the diagnostics troubleshooting doc, §8 item 3) since neither blocks on anything; open Compono.Logging's Proposed ADR next, since it's genuinely ready; run the Compono.Http design spike in parallel or immediately after, since it's real work but bounded and well-scoped by §5a. This is still not a "large post-1.2 release" in the sense of an undifferentiated feature catalog — it is three specific, individually justified, differently-paced pieces of work, each with its own evidence and its own readiness state, exactly as the product owner's direction distinguishes admission from design from implementation.
Evidence index¶
- Direct reads:
docs/architecture/capability-admission.md,docs/roadmap/future-packages.md,docs/roadmap/post-mvp.md, ADRs 0044/0048/0051/0055/0056/0061, RESEARCH-0015/0016/0019/0023/0024/0025/ 0026/0027/0028,src/Compono/CallVerifier.cs,src/Compono.Http/TestHttpHandler.cs,src/Compono.Http/HttpResponseRegistrationBuilder.cs,src/Compono.Http/HttpResponseRegistration.cs,src/Compono.Logging/LogVerificationBuilder.cs,src/Compono.Logging/CapturedLogEntry.cs,src/Compono.Logging/LogEntryCollector.cs,src/Compono.Options/*,docs/packages/compono-options.md,docs/packages/compono-xunitv3.md,skills/compono/references/registrations-profiles-and-scopes.md,skills/compono/references/options.md. git log/git showagainst this repo for PRs #133/#134/#135/#136/#137 and tag history (v1.0.0throughv1.2.1).- Read-only inspection of
/Users/ncipollina/source/repos/layered-craft/alexa-vox-craft(branchmain, commits3245d95/2cce6a6) —Compono.Http,Compono.Logging,Compono.Options, andCompose<TProfile, TConfig>usage searched directly; no modifications made. - Read-only inspection of
/Users/ncipollina/source/repos/ncipollina/cosmere-tracker(branchmain, commit4d25e14) — the corrected path (2026-09-11 revision);Directory.Packages.props,ClientTestProfile.cs,HttpMessageHandlerExtensions.cs,PersistenceTestProfile.cs,BatchRepositoryTests.cs,MissingIndexConfigurationTests.cs, and every[Compose<...>]call site searched directly; no modifications made.
Links¶
- Feeds a
ProposedADR for Candidate 3 (Compono.Loggingstructured verification) directly, pertasks/design.md— §5a resolves its open questions, no further research needed first. - Feeds a short, focused design-dive (
tasks/design.md) for Candidates 1 and 2 (Compono.Httpbody + header matching) before any ADR — §5a names the exact architectural fork that spike must resolve. - Feeds two independent documentation-only PRs, neither requiring an ADR: Candidate 5's profile/Options doc-and-skill changes, and Candidate 7's
CS9057-troubleshooting-doc half. - Feeds a designed (not yet added) skill eval scenario for context-aware Options composition (Candidate 5) — for whoever next touches the
componoskill's eval suite. - Does not feed any ADR or Plan for Candidate 4 (Options follow-ups, including verification) or Candidate 6 (TestDoubles) — recorded as "already adequately supported," per
capability-admission.md's own outcome table; the correctedcosmere-trackerevidence was checked explicitly and doesn't change either verdict.