diff --git a/.agents/skills/engineering-workflow/SKILL.md b/.agents/skills/engineering-workflow/SKILL.md
index f6cc7358..782c2ab4 100644
--- a/.agents/skills/engineering-workflow/SKILL.md
+++ b/.agents/skills/engineering-workflow/SKILL.md
@@ -106,6 +106,7 @@ otherwise favors.
| When you're about to... | Read |
|---|---|
+| Decide whether a proposed capability, feature, integration, or new package belongs in Compono at all, before any design work starts | [`docs/architecture/capability-admission.md`](../../../docs/architecture/capability-admission.md) (standalone; read it directly, not via `references/`) |
| Decide where an architecture/feature decision belongs, run a design dive (light or deep) before writing code, write/reference an ADR (`docs/adr/`), or write/track a plan (`docs/plans/`) | `references/design-decisions.md` |
| Write or review any C# (naming, nullable, async, DI, error handling, file layout) | `references/coding-standards.md` |
| Add or change tests | `references/testing.md` |
diff --git a/.agents/skills/engineering-workflow/references/design-decisions.md b/.agents/skills/engineering-workflow/references/design-decisions.md
index 305a7e3b..92d70294 100644
--- a/.agents/skills/engineering-workflow/references/design-decisions.md
+++ b/.agents/skills/engineering-workflow/references/design-decisions.md
@@ -11,6 +11,17 @@ already decided" is more useful than one that re-derives a solved problem
from first principles, and it's a fast check relative to the cost of
designing around a wrong assumption.
+If the request is a genuinely new capability, a material expansion of an
+existing package's public surface, or a new extension/integration
+package — not a bug fix or straightforward implementation against an
+already-`Accepted` ADR — run it through
+[`docs/architecture/capability-admission.md`](../../../../docs/architecture/capability-admission.md)
+**before** deciding light vs. deep dive below. That page is the standalone,
+current process for whether something belongs in Compono at all; a
+candidate that doesn't clear it doesn't get an ADR of its own, light or
+deep. `docs/adr/0029-...` and `docs/adr/0039-...` are that page's
+underlying decisions, not a substitute for reading it directly.
+
## Where decisions live
Four places, each with a different job — don't blur them together:
diff --git a/.agents/skills/engineering-workflow/tasks/design.md b/.agents/skills/engineering-workflow/tasks/design.md
index 0d2857c6..a4825b6d 100644
--- a/.agents/skills/engineering-workflow/tasks/design.md
+++ b/.agents/skills/engineering-workflow/tasks/design.md
@@ -40,6 +40,14 @@ alternatives are even legal:
the docs, here's where" (`design-decisions.md`'s opening rule). A design
session grounded in what's already decided beats one that re-derives a
solved problem.
+
+ If the request is a genuinely new capability, a material expansion of
+ an existing package's public surface, or a new extension/integration
+ package, run it through
+ [`docs/architecture/capability-admission.md`](../../../../docs/architecture/capability-admission.md)
+ first — a candidate that doesn't clear that process doesn't get an ADR
+ at all, and this task shouldn't reconstruct that admission reasoning
+ from `docs/adr/0029-...`/`docs/adr/0039-...` from scratch each time.
2. **Decide light vs. deep**, per `design-decisions.md`:
- **Light** — problem and solution shape are already clear (adding a
provider following a pattern another provider already uses, adopting
diff --git a/.github/scripts/inspect-packed-nupkgs.sh b/.github/scripts/inspect-packed-nupkgs.sh
index 8a0b0fdf..cd91a40e 100755
--- a/.github/scripts/inspect-packed-nupkgs.sh
+++ b/.github/scripts/inspect-packed-nupkgs.sh
@@ -245,7 +245,7 @@ main() {
}
local pkg nupkg extract_dir extra_paths nuspec
- for pkg in Compono Compono.XunitV3 Compono.NSubstitute Compono.Bogus Compono.TUnit Compono.TestDoubles Compono.DependencyInjection Compono.Http Compono.Logging Compono.MSTest Compono.NUnit; do
+ for pkg in Compono Compono.XunitV3 Compono.NSubstitute Compono.Bogus Compono.TUnit Compono.TestDoubles Compono.DependencyInjection Compono.Http Compono.Logging Compono.MSTest Compono.NUnit Compono.Options; do
nupkg=$(find "$pack_output" -maxdepth 1 -iname "${pkg}.[0-9]*.nupkg" | head -1)
if [ -z "$nupkg" ]; then
echo "FAIL: no .nupkg found for $pkg in $pack_output" >&2
@@ -339,6 +339,14 @@ main() {
assert_exact_pin_dependency "$nuspec" "$pkg" "Compono"
assert_dependency_range "$nuspec" "$pkg" "NUnit" "$authoritative_json"
;;
+ Compono.Options)
+ assert_manifest_field "$nuspec" "$pkg" "title" "Compono — Configuration/Options Testing Support"
+ assert_exact_pin_dependency "$nuspec" "$pkg" "Compono"
+ # Per-TFM range, same shape as Compono.Logging's Microsoft.Extensions.Logging.Abstractions
+ # dependency above (net11.0 carries no explicit dependency entry - satisfied by that TFM's
+ # own shared framework, confirmed against a real local pack).
+ assert_dependency_range_per_tfm "$nuspec" "$pkg" "Microsoft.Extensions.Options" "$packages_props"
+ ;;
esac
done
diff --git a/.github/workflows/aot-validation.yaml b/.github/workflows/aot-validation.yaml
index 3d283db5..0c646910 100644
--- a/.github/workflows/aot-validation.yaml
+++ b/.github/workflows/aot-validation.yaml
@@ -1,6 +1,6 @@
name: AOT Validation
-# ADR-0041 Amendment 7: permanent, CI-blocking Native AOT smoke gate for the eight existing
+# ADR-0041 Amendment 7: permanent, CI-blocking Native AOT smoke gate for the nine existing
# test/*.AotSmokeTest projects, replacing the previous manual-only "run it by hand before release"
# verification.
#
@@ -8,7 +8,7 @@ name: AOT Validation
# skipped by trigger-level path filtering leaves its required status check `Pending` rather than
# reporting success, under GitHub's required-check semantics - that would block a PR indefinitely
# instead of passing it on an AOT-irrelevant change. Selectivity happens *inside* the workflow
-# instead: the `changes` job below computes which of the eight legs are actually applicable from the
+# instead: the `changes` job below computes which of the nine legs are actually applicable from the
# PR's changed files (a small repository-owned `git diff` script, not a third-party changed-files
# action), each leg's own publish-and-run job runs behind an `if:` reading that output (an
# inapplicable leg reports an ordinary skipped conclusion, never a missing status), and `aot-gate` -
@@ -56,14 +56,14 @@ jobs:
echo "Changed files:"
echo "$changed"
- all_legs='["Compono","Compono.Http","Compono.Logging","Compono.MSTest","Compono.NUnit","Compono.TestDoubles","Compono.TUnit","Compono.XunitV3"]'
+ all_legs='["Compono","Compono.Http","Compono.Logging","Compono.MSTest","Compono.NUnit","Compono.Options","Compono.TestDoubles","Compono.TUnit","Compono.XunitV3"]'
# A change to shared/core/generator infrastructure (or to this workflow itself) can affect
- # every packaged leg at once - run all eight rather than reflexively narrowing to only the
+ # every packaged leg at once - run all nine rather than reflexively narrowing to only the
# paths that happened to change (ADR-0041 Amendment 7's own "do not run it reflexively, but
# do not under-run it either" balance).
if echo "$changed" | grep -qE '^(src/Compono/|src/Compono\.Generators/|Directory\.Packages\.props|Directory\.Build\.(props|targets)|test/Directory\.Build\.(props|targets)|\.github/workflows/aot-validation\.yaml)'; then
- echo "Core/generator/shared-config change detected - running all eight legs."
+ echo "Core/generator/shared-config change detected - running all nine legs."
echo "legs=$all_legs" >> "$GITHUB_OUTPUT"
exit 0
fi
@@ -77,12 +77,13 @@ jobs:
echo "$changed" | grep -q '^src/Compono\.Logging/' && add_leg "Compono.Logging"
echo "$changed" | grep -q '^src/Compono\.MSTest/' && add_leg "Compono.MSTest"
echo "$changed" | grep -q '^src/Compono\.NUnit/' && add_leg "Compono.NUnit"
+ echo "$changed" | grep -q '^src/Compono\.Options/' && add_leg "Compono.Options"
echo "$changed" | grep -q '^src/Compono\.TestDoubles/' && add_leg "Compono.TestDoubles"
echo "$changed" | grep -q '^src/Compono\.TUnit/' && add_leg "Compono.TUnit"
echo "$changed" | grep -q '^src/Compono\.XunitV3/' && add_leg "Compono.XunitV3"
# A change scoped to one leg's own AotSmokeTest project only needs that leg re-run, not all
- # eight - extract which leg(s) directly from the changed paths.
+ # nine - extract which leg(s) directly from the changed paths.
for proj in $(echo "$changed" | grep -oE '^test/[^/]+\.AotSmokeTest/' | sed -E 's#^test/(.+)\.AotSmokeTest/#\1#' | sort -u); do
add_leg "$proj"
done
diff --git a/.github/workflows/package-validation.yaml b/.github/workflows/package-validation.yaml
index e26d5f06..f434db97 100644
--- a/.github/workflows/package-validation.yaml
+++ b/.github/workflows/package-validation.yaml
@@ -37,7 +37,7 @@ jobs:
# process environment by GitHub Actions itself, so this survives across the separate `run:`
# steps below (each its own shell process) with no extra plumbing - deliberately not a Bash
# array, which would only live for the one step that declared it.
- PACKAGES: "Compono Compono.XunitV3 Compono.NSubstitute Compono.Bogus Compono.TUnit Compono.TestDoubles Compono.DependencyInjection Compono.Http Compono.Logging Compono.MSTest Compono.NUnit"
+ PACKAGES: "Compono Compono.XunitV3 Compono.NSubstitute Compono.Bogus Compono.TUnit Compono.TestDoubles Compono.DependencyInjection Compono.Http Compono.Logging Compono.MSTest Compono.NUnit Compono.Options"
steps:
- uses: actions/checkout@v7
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 120000
index 00000000..47dc3e3d
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1 @@
+AGENTS.md
\ No newline at end of file
diff --git a/Compono.slnx b/Compono.slnx
index 94d60622..abbc6aca 100644
--- a/Compono.slnx
+++ b/Compono.slnx
@@ -12,6 +12,7 @@
+
@@ -26,6 +27,7 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/README.md b/README.md
index 5263a9aa..090afba9 100644
--- a/README.md
+++ b/README.md
@@ -47,6 +47,7 @@ Compono determines **how** those requirements are satisfied.
| `Compono.Logging` | `Microsoft.Extensions.Logging` testing support (`UseLogging()`, `CapturingLogger`) | [](https://www.nuget.org/packages/Compono.Logging) | [](https://www.nuget.org/packages/Compono.Logging) |
| `Compono.MSTest` | MSTest integration | [](https://www.nuget.org/packages/Compono.MSTest) | [](https://www.nuget.org/packages/Compono.MSTest) |
| `Compono.NUnit` | NUnit integration (no `[TestFixture]` required) | [](https://www.nuget.org/packages/Compono.NUnit) | [](https://www.nuget.org/packages/Compono.NUnit) |
+| `Compono.Options` | `Microsoft.Extensions.Options` testing support (`TestOptionsSource`, `UseOptions()`) | [](https://www.nuget.org/packages/Compono.Options) | [](https://www.nuget.org/packages/Compono.Options) |
## Example
diff --git a/docs/adr/0061-compono-options-testing-support.md b/docs/adr/0061-compono-options-testing-support.md
new file mode 100644
index 00000000..966c2503
--- /dev/null
+++ b/docs/adr/0061-compono-options-testing-support.md
@@ -0,0 +1,787 @@
+# [ADR-0061] `Compono.Options`: First-Class .NET Configuration/Options Testing Support
+
+**Status:** Accepted
+
+**Date:** 2026-09-07 (revised 2026-09-08 — dogfooding validation, `IOptionsSnapshot`
+semantics research, public object model, and registration/identity resolved,
+plus source-disposal and concurrency final resolutions; see "Revision
+(2026-09-08)" below Context and "Acceptance" at the end. `Accepted`
+2026-09-08 — see "Acceptance.")
+
+**Decision Makers:** Nick Cipollina, Claude (design deep dive)
+
+## Context
+
+`Microsoft.Extensions.Options`'s `IOptions`/`IOptionsSnapshot`/
+`IOptionsMonitor` is the standard, idiomatic way a modern .NET
+application receives strongly-typed configuration. Any real application
+composed with Compono that reads configuration this way needs its test
+doubles composed the same way every other dependency is.
+
+This ADR is the outcome of an admission investigation and reassessment —
+[RESEARCH-0028](../research/0028-compono-options-configuration-admission-research.md) —
+run against
+[`docs/architecture/capability-admission.md`](../architecture/capability-admission.md)'s
+Gate A/Gate B process, triggered by an explicit product-owner request and
+then reassessed the same day against a sharpened version of that request
+(composition ergonomics as legitimate value, not merely "wrap an
+already-simple API"). Both gates cleared; see that research document for
+the full evidence trail and per-slice decomposition (Configuration,
+`IOptions` alone, and Options validation were each investigated and
+separately concluded *not* to need new capability). This ADR records the
+problem this capability solves and the recommended architecture — it does
+not re-derive RESEARCH-0028's evidence.
+
+**The problem has two independent, evidenced dimensions**, neither of
+which alone would have been sufficient (RESEARCH-0028 §1, §13):
+
+1. **Correctness.** `IOptionsMonitor`/`IOptionsSnapshot` have no
+ first-party test double. The community's own standard answer — a
+ hand-rolled fake, most visibly
+ [Ben Foster's widely-cited `TestOptionsMonitor`](https://benfoster.io/blog/20200610-testing-ioptionsmonitor/) —
+ is demonstrably incomplete: `Get(name)` ignores `name` entirely (named
+ options silently broken), only one `OnChange` subscriber is ever
+ honored (a plain field assignment, not a real multicast event), and the
+ returned `IDisposable` is a no-op (`Mock.Of()`) that
+ doesn't actually unsubscribe anything.
+2. **Composition coherence.** A Compono consumer whose SUT depends on
+ `IOptions` *and* `IOptionsMonitor`/`IOptionsSnapshot` for the
+ same settings type has to hand-wire each one separately today, with
+ nothing preventing them from silently drifting inconsistent. This is
+ the same category of value `CompositionBuilder.Share()`
+ ([ADR-0056](0056-composition-builder-share-graph-wide-sharing.md)) and
+ `Compono.Bogus` ([ADR-0027](0027-compono-bogus-package-design.md))
+ were admitted on — composition-native, discoverable, consistent
+ behavior a consumer would otherwise reinvent, slightly differently,
+ every time.
+
+**What "coherent" does and does not mean here** — the central semantic
+question this ADR has to answer, not assume: `IOptions`,
+`IOptionsSnapshot`, and `IOptionsMonitor` are **not** interchangeable
+views over one mutable value. A design that makes all three reactively
+track one mutable value would be **more surprising than the real thing**,
+not more correct — `IOptions` genuinely doesn't change in production,
+and a Compono fake that makes it change would silently mismatch the real
+contract it claims to model. **Precise definition, settled by this
+revision (see below):** "coherent" means all three interfaces originate
+from **one explicitly test-configured source of truth** per settings type
+— but each interface still exposes exactly its own real, distinct
+observable contract over that source. A shared origin, not shared
+behavior.
+
+## Revision (2026-09-08): dogfooding validation, real `OptionsManager` semantics, and the resulting object model
+
+A follow-up design review (2026-09-08) found that this ADR's original
+Snapshot mapping ("one Compono resolution stands in for one DI scope") was
+a Compono-invented analogy asserted without checking Microsoft's actual
+implementation, and that "single class vs. two" and the exact registration
+identity/lifetime contract were left as open questions even though they
+determine public semantics, not just implementation shape. This revision
+resolves both **before** this ADR is fit for acceptance. Nothing in the
+original Context's problem statement, the correctness findings (subscriber
+exceptions, thread-safety posture), the Finding B boundary, or the
+verification/scope decisions changed — only the Snapshot semantics, the
+public object model, and the registration/identity contract, all
+superseded by this section and "Decision Outcome" below.
+
+### Dogfooding validation against real consumers
+
+Per RESEARCH-0028's identified consumers, both were inspected directly
+(read-only; neither repository was modified) for every real
+`IOptions`/`IOptionsSnapshot`/`IOptionsMonitor` usage:
+
+- **`alexa-vox-craft`** (`/Users/ncipollina/source/repos/layered-craft/alexa-vox-craft`) —
+ 32 files reference `IOptions`; **zero** reference
+ `IOptionsSnapshot`/`IOptionsMonitor`. Two real test-profile
+ registrations found:
+ - `test/AlexaVoxCraft.MediatR.Tests/TestKit/MediatRTestProfile.cs`:
+ `.Register(_ => new SkillServiceConfiguration {...})`
+ immediately followed by
+ `.Register>(context => Options.Create(context.Resolve()))`
+ — **two separately-maintained registrations for the same settings
+ type, in the same profile, with nothing enforcing they stay
+ consistent** — exactly the coherence risk this ADR's Context
+ identifies, concretely present in real code, today.
+ - `test/AlexaVoxCraft.Smapi.Tests/TestKit/SmapiHttpTestProfile.cs`:
+ `.Register>(context => Options.Create(new SmapiDeveloperAccessTokenOptions { ClientId = context.Resolve(), ... }))`
+ — the file's own comment documents this shape exists **specifically
+ because** `context.Resolve()`
+ throws `CompositionException` (ADR-0052 Finding B, reproduced live in
+ this real file) — the record type is never independently discovered
+ as a root anywhere in the project.
+- **`cosmere-tracker`** (`/Users/ncipollina/source/repos/ncipollina/cosmere-tracker`) —
+ 3 files reference `IOptions`; **zero** reference
+ `IOptionsSnapshot`/`IOptionsMonitor`.
+ `test/Cosmere.Tracker.Shared.Tests/TestKit/Profiles/PersistenceTestProfile.cs`:
+ `.Register>(() => Options.Create(new DynamoDbOptions {...}))`
+ — a simpler case, an inline literal with no nested resolve at all, no
+ Finding B involvement.
+
+**Selected dogfooding validation target: `MediatRTestProfile.cs`'s
+`IOptions` registration.** Chosen over the
+`Smapi`/`cosmere-tracker` cases because it's the one real, non-Finding-B-entangled
+example of exactly the coherence risk this ADR exists to close — two
+registrations for one settings type, wired by hand, with no structural
+guarantee they agree. `Compono.Options` is expected to collapse these two
+lines into one coherent registration backed by a single source, with the
+consistency guarantee built in rather than hand-maintained. It is
+**explicitly not** expected to improve the `SmapiHttpTestProfile.cs` case
+— that one is Finding-B-shaped, and this ADR's design deliberately doesn't
+touch Finding B (unchanged from the original ADR).
+
+**Honest finding, not manufactured:** neither repository exercises
+`IOptionsSnapshot` or `IOptionsMonitor` at all. There is **no real
+dogfooding evidence for the Monitor/Snapshot correctness dimension** —
+that half of this capability's justification rests entirely on external,
+well-documented community-fake-defect evidence (Ben Foster et al.,
+RESEARCH-0028 §7), not on friction observed in a real Compono consumer.
+This doesn't weaken Gate A/Gate B (Gate B was satisfied by explicit
+product-owner request, not dogfooding, exactly as `Compono.TUnit`/
+`Compono.NUnit` were) but it does mean this design's Monitor/Snapshot
+surface has not yet been pressure-tested against a real consumer's actual
+usage pattern — recorded as a genuine gap, not glossed over.
+
+### Real `IOptions`/`IOptionsSnapshot` semantics — researched, not assumed
+
+Confirmed directly against
+[dotnet/runtime's `OptionsManager.cs`](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Options/src/OptionsManager.cs)
+(the concrete type behind both interfaces in real Microsoft code) and
+[Options pattern - .NET | Microsoft Learn](https://learn.microsoft.com/en-us/dotnet/core/extensions/options):
+
+- **`OptionsManager` implements *both* `IOptions` and
+ `IOptionsSnapshot` — the same concrete class, not two related
+ ones.** `IOptions.Value` is literally `Get(Options.DefaultName)`;
+ `IOptionsSnapshot.Value`/`Get(name)` is the exact same code path.
+ There is **no type-level behavioral difference between `IOptions` and
+ `IOptionsSnapshot` at all.**
+- Results are cached **per-instance**, keyed by name (`OptionsCache`).
+ Calling `Get(name)` twice on the *same instance* returns the cached
+ value both times — no recomputation.
+- **`OptionsManager` itself has zero scope-awareness.** It
+ doesn't know or care whether it's registered Singleton or Scoped.
+- **The entire `IOptions` vs. `IOptionsSnapshot` behavioral
+ difference is purely a consequence of DI *registration lifetime*, not
+ anything in the type**: `IOptions` is registered Singleton (one
+ `OptionsManager` instance, and therefore one cache, for the whole
+ app's lifetime — hence "never changes"); `IOptionsSnapshot` is
+ registered Scoped (a *fresh* `OptionsManager` instance, with a fresh,
+ empty cache, per DI scope — hence "recomputed once per scope, fixed
+ within it").
+
+This is a materially better-grounded finding than this ADR's original
+"one Compono resolution stands in for one DI scope" analogy — that
+analogy turns out to be **exactly right**, but for a reason this ADR
+didn't originally have evidence for: real Microsoft achieves "fixed
+within a lifetime, fresh across lifetimes" purely by controlling *how many
+instances exist and when they're constructed*, using one interchangeable
+type. Compono already has an exact, existing, `Accepted` primitive for
+controlling instance count within a graph: `CompositionBuilder.Share()`
+vs. an ordinary (non-shared) `Register` factory. No new lifecycle
+concept needs to be invented — this maps onto real Compono mechanics
+already in production, not a Compono-specific fiction.
+
+## Decision Drivers
+
+- **Faithfulness over convenience.** A test double for a specific
+ interface contract is only useful if its observable behavior matches
+ that contract.
+- **No reflection, no hidden state** ([ADR-0001](0001-source-generation-first.md)) —
+ a hand-written, non-generated runtime package, the same shape
+ `Compono.Http` already established.
+- **Core `Compono` must never reference an integration package**
+ ([design-principles.md](../architecture/design-principles.md)) — this
+ package depends on `Microsoft.Extensions.Options` and core `Compono`
+ only, never the reverse.
+- **ADR-0052 Finding B is a hard boundary, not a design target.** Unchanged
+ from the original ADR — see "ADR-0052 Finding B boundary" below.
+- **Read naturally both inline and inside a profile.** Unchanged — see
+ `MediatRTestProfile.cs`'s real usage above for what "naturally" means in
+ practice.
+- **Match real `OptionsMonitor`'s actual robustness posture, not an
+ imagined stricter one.** Unchanged from the original ADR (subscriber-throw,
+ locking posture) — confirmed directly against source, not re-litigated
+ in this revision.
+- **Prefer existing Compono primitives over inventing a new lifecycle
+ concept.** New this revision, directly from the `OptionsManager`
+ finding above: if real Microsoft achieves the `IOptions`/
+ `IOptionsSnapshot` distinction purely through instance-count control,
+ this design should too, using `Share()`/plain `Register` rather
+ than inventing a Compono-specific "snapshot lifecycle."
+- **Package-boundary discipline** ([ADR-0039](0039-future-extension-package-admission-gate-and-release-sequence.md)) —
+ `Compono.DependencyInjection` remains the wrong home (unchanged,
+ RESEARCH-0028 §12).
+
+## Considered Options
+
+**Consumer entry point** (unchanged from the original ADR — see "Why
+Option 1" below, reasoning unchanged):
+1. A single hand-written class per settings type, constructed directly by
+ the test, wired into composition via one builder call.
+2. Three independent builder extension methods, each registering one
+ interface separately.
+3. A universal auto-composing stage-4-6 provider.
+
+**Public object model** (new this revision):
+1. **One public source type per settings type**, holding the current
+ default/named values and the change-notification machinery, directly
+ implementing `IOptionsMonitor` itself (the one genuinely live,
+ singleton-shaped interface) plus a Compono-native mutation surface
+ (illustratively, `.Set(...)`/named overloads). A small **internal**
+ frozen-view type — never named by consumer code — implements
+ `IOptions`/`IOptionsSnapshot`, constructed fresh from the
+ source's current state each time one is produced.
+2. **One public type implementing all three Microsoft interfaces at
+ once** (`IOptions`, `IOptionsSnapshot`, `IOptionsMonitor`
+ simultaneously on one object).
+3. **Three separate public types**, one per Microsoft interface, each
+ independently wrapping a shared internal state object.
+
+**`IOptions`/`IOptionsSnapshot` instance/identity model** (new this
+revision, replacing the original "frozen at resolution" framing with a
+mechanism, not just a behavior):
+1. Register `IOptions` **with `Share()`** (one frozen-view instance
+ for the whole composition graph — matches Singleton/"one
+ `OptionsManager` for the app's life"); register
+ `IOptionsSnapshot` as an **ordinary, non-shared** `Register`
+ factory (Compono invokes the factory fresh each time it's resolved,
+ producing a new frozen-view instance per resolution — matches Scoped/
+ "fresh `OptionsManager` per scope").
+2. Register both with `Share()` (one frozen instance for the whole
+ graph, no distinction between the two interfaces at all).
+3. Register both as ordinary, non-shared factories (a fresh frozen view
+ every single resolution, for both interfaces).
+
+**Change-notification robustness, unconfigured-named-option behavior,
+verification:** unchanged from the original ADR (see "Decision Outcome,"
+carried forward below) — this revision found no new evidence requiring
+either to change.
+
+## Decision Outcome
+
+**Chosen, per axis:** entry point — **Option 1** (unchanged); public
+object model — **Option 1**; `IOptions`/`IOptionsSnapshot` identity
+model — **Option 1**; change-notification robustness, unconfigured-named-option
+behavior, and verification — all **carried forward unchanged** from the
+original ADR (reasoning below, condensed; full reasoning is this ADR's
+git history / the original 2026-09-07 text, superseded here per this
+repo's "later fact gets its own dated update" convention rather than
+silently rewritten).
+
+### Entry point: Option 1 (unchanged reasoning)
+
+Option 3 (universal auto-composing provider) is rejected: it would call
+`context.Resolve()` for an arbitrary `T` inside its own `TryProvide`,
+hitting ADR-0052 Finding B exactly whenever `T` isn't independently
+discovered elsewhere — the identical wall `SmapiHttpTestProfile.cs`
+(above) already hits by hand. Option 2 (three independent registrations)
+is rejected: it's exactly `MediatRTestProfile.cs`'s existing shape, and
+doesn't solve the coherence problem at all. **Option 1**: the test
+constructs a single per-settings-type source object directly, then one
+builder call wires all three interfaces from it — avoids Finding B
+entirely (the test supplies the value; nothing resolves `T` from inside a
+factory), needs no new core extension point, reads identically inline and
+inside a profile.
+
+### Public object model: Option 1, chosen against your stated preference — with the reasoning that resolves it
+
+You noted a strong preference against "one class implementing multiple
+Microsoft interfaces merely because implementation can be shared." That
+preference is correct **as a default heuristic** and this decision doesn't
+override it casually — it resolves it against the specific evidence
+above, which changes the premise: **`IOptions` and `IOptionsSnapshot`
+are not two interfaces with different lifetime semantics that happen to
+share implementation for convenience — in real Microsoft code they are
+the exact same behavior, exposed through two interface names, with the
+only real difference being how many instances of that one behavior exist**
+(§"Real `IOptions`/`IOptionsSnapshot` semantics," above). Combining
+them here is not a shortcut; it's matching the real architecture 1:1,
+the same standard this ADR already holds every other fidelity decision to.
+
+**`IOptionsMonitor` is different in kind, not just in cardinality** — it
+is a genuinely separate, live-reactive contract in real Microsoft code
+too (`OptionsMonitor`, a distinct class from `OptionsManager`). This
+design keeps it conceptually distinct: the one **public** type per
+settings type is the *source* — the thing the test constructs, mutates,
+and subscribes to — and it directly implements `IOptionsMonitor`,
+because Monitor's contract ("read the current live value, subscribe to
+changes") is exactly what a mutable source naturally *is*, not a
+retrofit. The frozen-view type behind `IOptions`/`IOptionsSnapshot`
+is a small, **internal** implementation detail a consumer never names —
+it's reached only through the standard interface types, the same way a
+consumer today never thinks about `OptionsManager`'s own concrete type
+either. Option 2 (one type implementing all three at once, including
+Monitor) is rejected for the reason you gave: Monitor's live semantics
+and the frozen views' fixed semantics are genuinely different in kind, and
+collapsing them into one object would obscure that distinction rather
+than express it. Option 3 (three fully separate public types) is
+rejected: it would either duplicate the frozen-view logic for `IOptions`
+and `IOptionsSnapshot` (contradicting the finding that they're the same
+behavior) or force an artificial public split between two things that are
+genuinely one.
+
+**Resulting public model:** one public type per settings type (name not
+finalized) — call it conceptually the **Options source** — is what a test
+constructs, configures, mutates, and passes to the one wiring call. It
+directly implements `IOptionsMonitor`. `IOptions` and
+`IOptionsSnapshot` are satisfied by an internal frozen-view type the
+wiring call constructs from the source; a consumer's code only ever sees
+these as the ordinary Microsoft interfaces.
+
+### `IOptions`/`IOptionsSnapshot` identity model: Option 1, built entirely on existing Compono primitives
+
+Directly following from the `OptionsManager` finding: the wiring call
+registers `IOptions` **via `Share()`** — one frozen-view instance,
+constructed once (capturing the source's value at that moment) and reused
+for every subsequent request for `IOptions` within the same graph,
+matching Singleton/"one instance for the app's life." It registers
+`IOptionsSnapshot` as an **ordinary `Register` factory, deliberately
+without `Share()`** — per Compono's own existing, unchanged
+`StoreSharedValue` semantics (`src/Compono/CompositionContext.cs`), a
+plain registration's factory is re-invoked on every resolution unless the
+type is shared, so each `IOptionsSnapshot` resolution naturally
+produces a **fresh** frozen view capturing the source's *current* state at
+that moment — matching Scoped/"fresh instance per scope," with "one
+Compono resolution" now precisely and correctly standing in for "one DI
+scope" **because Compono's own registration semantics already produce
+exactly that shape**, not because this ADR invented a special case for it.
+
+`IOptionsMonitor` is registered the same way `IOptions` is
+(`Share()` — one instance per graph) since it *is* the source object
+itself, and the source only ever needs one identity per graph regardless
+of how many places request it.
+
+**This directly answers your registration/composition questions:**
+
+- Repeated resolution of `IOptions` returns the **same** frozen wrapper
+ (shared).
+- Repeated resolution of `IOptionsSnapshot` returns a **new** frozen
+ view **each time**, reflecting whatever the source's current state is
+ at that moment (not shared) — matching real Scoped-per-request
+ semantics as closely as a scope-free environment honestly can.
+- `IOptionsMonitor` has **stable identity** — the same source instance
+ every time (shared), consistent with its real Singleton registration.
+- An explicit `Register>(...)`/`Register>(...)`/
+ `Register>(...)` written by the consumer **after**
+ `Compono.Options`'s own wiring call interacts through Compono's ordinary,
+ unchanged first-registration-wins rule
+ (`src/Compono/CompositionBuilder.cs`) — no special-cased precedence is
+ invented for this package. A consumer who explicitly overrides one of
+ the three still gets ordinary, predictable Compono behavior, just
+ without this package's coherence guarantee for the overridden interface
+ specifically.
+- `Share()`'s role: internal to how the wiring call registers
+ `IOptions`/`IOptionsMonitor` — a consumer never calls `Share()`
+ themselves for these types; the package uses the primitive on the
+ consumer's behalf.
+- Profiles: unaffected — the wiring call is one more ordinary
+ `CompositionBuilder` call, exactly like `MediatRTestProfile.cs`'s
+ existing `Register>(...)` call it's meant to replace.
+
+### Change-notification robustness, unconfigured-named-option behavior, verification (carried forward, condensed)
+
+- **Change notification** matches real `OptionsMonitor` exactly — a
+ plain event, synchronous invocation, no per-subscriber exception
+ isolation, no locking beyond what a compiler-generated event already
+ guarantees. Confirmed directly against `OptionsMonitor.cs`
+ (`_onChange` is a plain `event Action?`; a throwing
+ subscriber blocks remaining ones; no explicit locking anywhere). This
+ package's one real correctness addition over the naive community fake:
+ a genuine per-subscription `IDisposable` that actually unsubscribes
+ (`-=` against the internal event) — the one concrete bug (RESEARCH-0028
+ §7) a correctly-used C# event fixes for free.
+- **Unconfigured named option: throws, reaffirmed with a sharper
+ justification.** Researched further this revision: real
+ `IOptionsFactory.Create(name)` for a name with no matching
+ `IConfigureOptions`/`IConfigureNamedOptions` silently returns a
+ plain `new TOptions()` — no exception, no signal anything was
+ unconfigured (confirmed against the documented `IOptionsFactory`
+ contract on the Learn page: it applies whichever registered
+ configuration delegates match a name and returns the result
+ regardless of whether any did). This ADR's throw-instead choice is
+ therefore a **real, disclosed divergence from production behavior**,
+ not a fidelity-neutral default — but it is not a novel invention
+ either: it's the exact same tradeoff Compono's own `Compono.TestDoubles`
+ already made and shipped, in the very consumer this ADR validated
+ against. `MediatRTestProfile.cs`'s own comments (above) describe
+ `IHandlerInput.RequestEnvelope` and `IAttributesManager.Session` as
+ deliberately generated *configuration-required* under
+ [ADR-0045](0045-testdoubles-configuration-required-members.md) —
+ "more honest than implicit auto-population or silent-null defaults, at
+ the cost of one explicit line per test that needs it" — the identical
+ justification this ADR is making for unconfigured named options,
+ already an established, `Accepted`, real-consumer-validated Compono
+ precedent, not a general principle asserted in the abstract. Reaffirmed:
+ throwing remains the decision.
+- **Verification** remains deferred, not rejected — no evidenced demand,
+ including from the dogfooding validation above (neither real consumer
+ needed to verify change-notification call counts).
+
+### Source disposal: not `IDisposable`/`IAsyncDisposable` (resolved 2026-09-08, final)
+
+The public Options source object does **not** implement `IDisposable` or
+`IAsyncDisposable`. It owns no production resource requiring disposal — no
+file watcher, no real `IChangeToken`, no DI scope, no configuration
+provider. The correct ownership boundary is per-subscription: the
+`IDisposable` returned by `OnChange` is what a test disposes to
+unregister that specific listener, exactly matching real
+`OptionsMonitor`'s own per-registration disposal shape (its `Dispose()`
+tears down change-token subscriptions that don't exist in this design at
+all — there is nothing analogous for the source itself to dispose).
+Giving the source its own `Dispose()` would imply a lifecycle/ownership
+contract Compono's broader composition model deliberately doesn't have —
+`CompositionRow`/`CompositionScope`/`Composer` own no disposal contract of
+their own either (`Compono.DependencyInjection`'s `AsServiceProvider()`
+bridge follows the identical rule, per ADR-0047). This is not left as an
+implementation convenience question — no `Dispose()` method is added to
+the source under any circumstance, including "just to clear all
+subscribers at once"; a test that needs that disposes each subscription
+individually.
+
+### Concurrent access to one source instance (resolved 2026-09-08, final)
+
+**Reframed from the original ADR's "concurrent test execution" framing,
+which was the wrong question.** A Compono.Options source belongs to one
+composition/test — sharing one mutable source instance across independent
+*parallel tests* is not a supported scenario and does not drive this
+design, the same way no other Compono composition primitive is designed
+for cross-test sharing. The real, legitimate concern is concurrency
+**within** one test/composition: a SUT may read `CurrentValue` on one
+thread while another triggers a change, multiple consumers may subscribe/
+unsubscribe concurrently, and a named value may be read while a different
+name is being changed.
+
+**Behavioral contract (architectural; the primitive that satisfies it is
+implementation-level, per Open Questions below):**
+
+- The source remains internally valid under concurrent reads, changes,
+ subscriptions, and unsubscriptions — no operation may observe corrupted
+ or partially-mutated internal state.
+- Named-value storage must not become structurally corrupted by concurrent
+ mutation of different names, or of the same name.
+- Subscribe/unsubscribe remains safe under concurrent calls, including
+ concurrent with an in-flight change notification.
+- **A change operation establishes the new current value before invoking
+ any change callback** — a callback that reads `CurrentValue`/`Get(name)`
+ during its own invocation observes the changed value, never the stale
+ one, matching real `OptionsMonitor`'s own cache-then-invoke ordering
+ (`InvokeChanged`: `_cache.TryRemove(name)` then recompute, *then*
+ `_onChange?.Invoke(...)`).
+- Notifications remain synchronous (unchanged from the original ADR).
+- Notification ordering follows ordinary multicast-event behavior, already
+ decided above — no additional ordering guarantee beyond that.
+- **No stronger transactional or cross-operation ordering guarantee is
+ promised** — e.g., no atomicity across a multi-name batch change, no
+ guaranteed ordering between two threads racing to change different
+ names, beyond each individual operation being internally safe. Inventing
+ either would be machinery with no evidenced need, exactly the kind of
+ speculative robustness this ADR's Decision Drivers already reject
+ elsewhere (§"Change-notification robustness").
+
+This contract removes concurrency from the unresolved/open-question list
+— the *behavior* is now decided. The concrete synchronization primitive
+(a `lock`, a `ConcurrentDictionary`, or another approach) is deliberately
+left to implementation planning, since more than one primitive can satisfy
+this contract and the choice has no public-observable consequence.
+
+### Positive Consequences
+
+- A correct, reusable `IOptionsMonitor` fake closes a real,
+ community-documented correctness gap.
+- The object model and identity contract close the exact coherence risk
+ found live in `MediatRTestProfile.cs` — one source, one wiring call,
+ `IOptions`/`IOptionsSnapshot`/`IOptionsMonitor` structurally
+ unable to drift apart.
+- The `IOptions`/`IOptionsSnapshot` identity model is built entirely
+ from existing, `Accepted` Compono primitives (`Share()`, ordinary
+ `Register` semantics) — no new lifecycle concept invented.
+- No new core extension point, no reflection, no generator dependency.
+
+### Negative Consequences
+
+- No automatic, no-registration composition for arbitrary `T` (unchanged
+ from the original ADR) — accepted, blocked on Finding B, out of scope.
+- The Monitor/Snapshot correctness case has no real dogfooding validation
+ — accepted, since Gate B was satisfied by explicit product-owner
+ request, but recorded honestly as a real gap this design has not been
+ pressure-tested against.
+- Matching real `OptionsMonitor`'s lack of per-subscriber exception
+ isolation (unchanged) — accepted, faithfulness over friendliness.
+- The unconfigured-named-option-throws choice diverges from real
+ `IOptionsFactory` — accepted, on the strength of the `Compono.TestDoubles`/
+ ADR-0045 precedent, not merely general principle.
+
+## Pros and Cons of the Options
+
+### Public object model: source implements Monitor; internal frozen-view type implements IOptions/Snapshot (chosen)
+
+- Good, because it matches real Microsoft architecture exactly
+ (`OptionsManager` implementing both frozen interfaces; a distinct
+ `OptionsMonitor` for the live one) rather than inventing a different
+ shape.
+- Good, because it keeps the one thing a consumer actually holds (the
+ source) conceptually simple — "the live, mutable thing," with the
+ frozen views as an implementation detail reached only through the
+ standard interfaces.
+- Bad, because a consumer inspecting the source's own type will see it
+ implements `IOptionsMonitor` specifically (not the frozen
+ interfaces) — a minor discoverability cost, mitigated by the wiring
+ call and documentation making the full picture obvious.
+
+### Public object model: one type implements all three
+
+- Good, because it's the smallest possible public surface.
+- Bad, because it obscures the genuine behavioral difference between
+ Monitor (live) and the two frozen interfaces (fixed) on one object —
+ exactly the ambiguity you flagged as a concern, and correctly so.
+
+### Public object model: three fully separate public types
+
+- Good, because each interface's public type is maximally simple in
+ isolation.
+- Bad, because it either duplicates frozen-view logic for two interfaces
+ that are genuinely the same behavior in real Microsoft code, or forces
+ an artificial split that doesn't reflect reality.
+
+### `IOptions`/`IOptionsSnapshot` identity: `Share()` for `IOptions`, plain `Register` for Snapshot (chosen)
+
+- Good, because it reproduces real Singleton-vs-Scoped behavior using
+ Compono's own existing, `Accepted` primitives — no new lifecycle
+ concept.
+- Good, because "one Compono resolution = one DI scope" is no longer an
+ asserted analogy; it's a direct, mechanical consequence of how
+ `Register` already behaves.
+- Bad, because a consumer has to understand that `IOptionsSnapshot`
+ resolved twice in the same graph yields two different frozen instances
+ — a real, if minor, surprise risk, mitigated by documentation and by
+ the fact that this exactly matches how two different DI scopes would
+ behave too.
+
+### `IOptions`/`IOptionsSnapshot` identity: both shared, no distinction
+
+- Good, because it's the simplest possible identity model.
+- Bad, because it silently drops the real, documented difference between
+ Singleton and Scoped registration — a fidelity gap, not a
+ simplification.
+
+### `IOptions`/`IOptionsSnapshot` identity: both fresh every resolution
+
+- Good, because it never risks stale data.
+- Bad, because it makes `IOptions` behave like `IOptionsSnapshot`
+ — contradicts real `IOptions`'s actual "one instance, once computed"
+ contract just as much as making it fully reactive would have.
+
+## Links
+
+- [RESEARCH-0028](../research/0028-compono-options-configuration-admission-research.md) —
+ the full admission investigation and reassessment.
+- [`docs/architecture/capability-admission.md`](../architecture/capability-admission.md) —
+ the governing admission process.
+- [ADR-0056](0056-composition-builder-share-graph-wide-sharing.md),
+ [ADR-0027](0027-compono-bogus-package-design.md) — the composition-
+ ergonomics-as-value precedent this capability's coherence dimension
+ follows.
+- [ADR-0051](0051-compono-http-handler-based-testing-package.md) — the
+ closest architectural precedent.
+- [ADR-0047](0047-compono-dependencyinjection-configured-resolution-bridge.md) —
+ confirms why this doesn't belong in `Compono.DependencyInjection`.
+- [ADR-0052](0052-compile-time-composition-discovery-boundary-for-registered-and-nested-resolved-types.md) —
+ Finding B, reproduced live in `SmapiHttpTestProfile.cs` (above).
+- [ADR-0018](0018-composition-profiles.md) — profiles need no new
+ mechanism to host this capability's registration call.
+- [ADR-0045](0045-testdoubles-configuration-required-members.md) — the
+ direct, real-consumer-validated precedent for this ADR's
+ unconfigured-named-option-throws decision.
+- [ADR-0001](0001-source-generation-first.md) — no-reflection-by-default.
+- [dotnet/runtime `OptionsMonitor.cs`](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Options/src/OptionsMonitor.cs) —
+ change-notification fidelity source.
+- [dotnet/runtime `OptionsManager.cs`](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Options/src/OptionsManager.cs) —
+ the 2026-09-08 revision's primary source for the `IOptions`/
+ `IOptionsSnapshot` identity model.
+- [Testing IOptionsMonitor - Ben Foster](https://benfoster.io/blog/20200610-testing-ioptionsmonitor/) —
+ the naive community fake this capability corrects.
+- `src/Compono/CompositionContext.cs` (`StoreSharedValue`),
+ `src/Compono/CompositionBuilder.cs` (`Register`) — the existing,
+ unchanged Compono mechanics the identity model is built on, inspected
+ directly this revision.
+- `/Users/ncipollina/source/repos/layered-craft/alexa-vox-craft`
+ (`MediatRTestProfile.cs`, `SmapiHttpTestProfile.cs`) and
+ `/Users/ncipollina/source/repos/ncipollina/cosmere-tracker`
+ (`PersistenceTestProfile.cs`) — the dogfooding validation sources
+ inspected this revision (read-only; neither repository modified).
+
+## Documentation consequences (recorded now, executed later)
+
+### `Compono.Options` package documentation (once implemented)
+
+Must cover: installation; basic setup; `IOptions`; `IOptionsSnapshot`;
+`IOptionsMonitor`; the identity model (shared `IOptions`/Monitor vs.
+fresh-per-resolution `IOptionsSnapshot`) explained in terms a consumer
+can reason about without needing to know `Share()` is involved
+internally; named options; deterministic changes; subscription/disposal
+semantics; inline usage; profile usage (with `MediatRTestProfile.cs`'s
+real before/after as a worked example, once implementation exists);
+relationship to ordinary `Microsoft.Extensions.Options` APIs; intentional
+non-goals (no real `IConfiguration`/change-token simulation, no
+`IOptionsFactory` pipeline, no DI-scope simulation); the
+unconfigured-named-option-throws divergence from real `IOptionsFactory`,
+stated plainly; the ADR-0052 Finding B limitation.
+
+### Configuration Cookbook — unchanged, still a required deliverable
+
+Preserved exactly as the original ADR recorded it — not weakened or
+dropped by this revision. Recorded in
+[`docs/roadmap/future-packages.md`](../roadmap/future-packages.md)'s
+"Documentation-only ideas" section:
+
+- Basic in-memory `IConfiguration` composition (`ConfigurationBuilder`,
+ `AddInMemoryCollection`, `Register`).
+- Layered configuration / test-specific overrides.
+- Reusable configuration through a profile.
+- `GetSection`/common consumption patterns.
+- Explicit routing guidance: ordinary Configuration for `IConfiguration`
+ itself; `Compono.Options` for the Options interfaces it owns; no
+ `Compono.Configuration` package exists.
+
+Whichever PR implements `Compono.Options` should treat this Cookbook work
+as part of its own definition of done.
+
+## Skill/eval consequences (recorded now, executed at implementation time)
+
+Unchanged from the original ADR: review/update `skills/compono/SKILL.md`'s
+detection table, add `references/options.md`, update `evals.json`, run the
+mandatory baseline-vs-updated skill-eval comparison — **not performed
+now**.
+
+## Open questions
+
+### Resolved this revision (were previously listed as open; now settled)
+
+- ~~Single class vs. two~~ — resolved: one public source type
+ implementing `IOptionsMonitor` directly; an internal frozen-view type
+ (never public) behind `IOptions`/`IOptionsSnapshot`.
+- ~~`IOptionsSnapshot` lifecycle/semantics~~ — resolved: matches real
+ `OptionsManager` architecture via `Share()` (for `IOptions`)
+ vs. plain `Register` (for `IOptionsSnapshot`); no invented
+ Compono-specific snapshot concept.
+- ~~Registration identity/lifetime for all three interfaces~~ — resolved,
+ precisely (see "Decision Outcome" above): `IOptions`/
+ `IOptionsMonitor` shared (one instance per graph);
+ `IOptionsSnapshot` fresh per resolution.
+- ~~Whether `Share()` has a role~~ — resolved: yes, used internally by
+ the wiring call for `IOptions`/`IOptionsMonitor`; never called
+ directly by the consumer.
+- ~~Unconfigured named-option behavior~~ — reaffirmed (throw), now backed
+ by the ADR-0045/`Compono.TestDoubles` precedent found in the dogfooding
+ validation, not just general principle.
+- ~~Disposal of the source object itself~~ — resolved 2026-09-08: no
+ `IDisposable`/`IAsyncDisposable` on the source; per-subscription
+ disposal only. See "Source disposal," above.
+- ~~Concurrent read/change interaction~~ — resolved 2026-09-08: reframed
+ from cross-test sharing (not a supported scenario) to concurrency
+ *within* one source instance, with an explicit behavioral contract. See
+ "Concurrent access to one source instance," above. The synchronization
+ *primitive* that satisfies the contract remains implementation-level
+ (see below).
+
+### Still genuinely open — implementation-level, not public-contract
+
+- **Exact public API naming** — the source type's name, its
+ `.Set(...)`-shaped mutation method(s), and the wiring call's name are
+ all illustrative only. Resolved by PLAN-0064's own naming-finalization
+ task before implementation begins, not by this ADR.
+- **Thread-safety implementation primitive** for the named-value store —
+ the *contract* it must satisfy (§"Concurrent access to one source
+ instance") is decided; whether a `lock`, a `ConcurrentDictionary`, or
+ another approach satisfies it most simply is an implementation choice
+ with no public-observable consequence.
+
+Both remaining items are naming/implementation choices only — neither
+changes what a consumer observes, and neither blocks acceptance.
+
+## Acceptance
+
+Every architectural/public-contract question this ADR's design pass
+identified is now resolved: the entry point, the public object model, the
+`IOptions`/`IOptionsSnapshot`/`IOptionsMonitor` identity and
+registration contract, named-option semantics (including the unconfigured-name
+divergence and its precedent), change-notification robustness, source
+disposal, and the concurrent-access contract. The one honest, disclosed
+gap — no real Compono consumer dogfoods `IOptionsMonitor`/
+`IOptionsSnapshot` specifically (§"Dogfooding validation," above) —
+doesn't block Gate A/Gate B (satisfied by explicit product-owner request,
+independent of dogfooding by design, exactly as `Compono.TUnit`/
+`Compono.NUnit` were) and doesn't leave any architectural question
+unresolved; it is carried forward into PLAN-0064 as deterministic
+contract-test coverage in lieu of real-consumer Monitor/Snapshot evidence,
+rather than glossed over. Accepted 2026-09-08.
+
+## Amendment 1 (2026-09-08): registration-precedence correction
+
+PLAN-0064's implementation surfaced a factual error in the "Decision
+Outcome" identity-model section above: the claim that an explicit consumer
+`Register>(...)`/`Register>(...)`/
+`Register>(...)` written after `UseOptions`'s own
+wiring call "interacts through Compono's ordinary, unchanged
+first-registration-wins rule" and that "a consumer who explicitly
+overrides one of the three still gets ordinary, predictable Compono
+behavior." **This describes behavior that does not exist.** Real
+`CompositionBuilder.Register` (`src/Compono/CompositionBuilder.cs`,
+ADR-0019) has no first-registration-wins or last-registration-wins
+override semantics at all for an exact-type collision: registering the
+same exact type more than once — directly, via a profile, or across two
+profiles — is a strict build-time conflict, thrown as
+`CompositionConfigurationException` at `Composer.Create`'s validation
+step, regardless of call order. Since `UseOptions` itself calls
+`Register>(...)`/`Register>(...)`/
+`Register>(...)` internally, a consumer's own explicit
+registration for any of the three collides with it and throws — it does
+not silently override it. Confirmed by a real test
+(`OrdinaryFirstRegistrationWinsPrecedence_HoldsUnchanged_ForAnExplicitConsumerOverride`,
+`test/Compono.Options.Tests/CompositionBuilderExtensionsTests.cs`) that
+asserts exactly this: `Composer.Create` throws
+`CompositionConfigurationException` when a consumer registration collides
+with `UseOptions`'s own.
+
+This does not change the ADR's core decision (the object model, the
+identity/lifetime contract, or `Share()`'s internal role) — those hold
+exactly as decided. It only corrects the override claim: **a consumer
+cannot selectively override one of the three Options interfaces while
+keeping `UseOptions`'s coherence guarantee for the other two** — there
+is no partial-override path. A consumer who genuinely needs different
+behavior for one interface must not call `UseOptions` for that settings
+type at all, and wires all three (or whichever it needs) by hand instead,
+same as before this package existed. "No special-cased precedence is
+invented for this package" remains true and is in fact the reason for this
+correction: Compono's real, unchanged, strict duplicate-registration rule
+applies here exactly as it does everywhere else, with no override
+exception carved out for `Compono.Options` — the ADR's original prose
+described that rule incorrectly, not this package behaving inconsistently
+with it.
+
+**Second, related correction — the "collapse these two lines into one"
+framing in "Selected dogfooding validation target" above, and what
+`Compono.Options` actually guarantees coherent.** `Compono.Options`
+guarantees coherence **among `IOptions`/`IOptionsSnapshot`/
+`IOptionsMonitor`** — all three originate from one `TestOptionsSource`
+by construction, and this is real and enforced. It does **not** guarantee
+coherence between the bare settings type `T` and those three interfaces
+merely because `T` happens to be registered elsewhere — `UseOptions`
+never touches a plain `Register()` registration at all, by design (§
+"ADR-0052 Finding B boundary": the source must be a value the test
+supplies directly, not something Compono resolves and re-wraps). The real
+`MediatRTestProfile.cs` dogfooding result (PLAN-0064) still needs a plain
+`Register(...)` registration alongside
+`UseOptions`, because a separate consumer in that same test project
+(`ServiceRegistrarTests`) depends on the bare type, not an Options
+interface — the two-registration shape did not literally collapse into
+one call. What changed is that both registrations are now sourced from
+the *same* local instance by the test author's own discipline (a single
+`var defaultSkillServiceConfiguration = new SkillServiceConfiguration {...}`
+passed to both `Register(() => defaultSkillServiceConfiguration)` and
+`new TestOptionsSource(defaultSkillServiceConfiguration)`) — coherent
+by construction *in that consumer*, not because `UseOptions` enforces
+any relationship to a separately-registered bare `T`. This is a narrowing
+of the original claim's scope, not a reversal of the admission decision
+(RESEARCH-0028/Gate A) — the coherence value `Compono.Options` provides
+among the three Options interfaces themselves remains exactly as
+evidenced and is what the dogfooding validation actually confirmed.
diff --git a/docs/adr/README.md b/docs/adr/README.md
index a28a964a..31fc23d7 100644
--- a/docs/adr/README.md
+++ b/docs/adr/README.md
@@ -122,3 +122,4 @@ the mechanics: numbering, status, and the index.
| [0058](0058-public-generator-facing-runtime-infrastructure.md) | Public Generator-Facing Runtime Infrastructure | Accepted |
| [0059](0059-compono-nunit-package-design.md) | Compono.NUnit Package Design | Accepted |
| [0060](0060-testdoubles-received-calls-and-clear-calls.md) | Compono.TestDoubles: `ReceivedCalls()` Retrospective Inspection and `ClearCalls()` | Accepted |
+| [0061](0061-compono-options-testing-support.md) | Compono.Options: First-Class .NET Configuration/Options Testing Support | Accepted |
diff --git a/docs/architecture/capability-admission.md b/docs/architecture/capability-admission.md
new file mode 100644
index 00000000..76e6c218
--- /dev/null
+++ b/docs/architecture/capability-admission.md
@@ -0,0 +1,507 @@
+# Capability & Package Admission
+
+**Audience:** anyone asking *"should Compono support X?"* — a contributor
+proposing a new capability, a maintainer triaging a feature request, or an
+agent evaluating a candidate package before starting a design pass.
+
+**What this page is:** the current, standalone, operational description of
+how Compono decides whether a proposed capability, feature, integration, or
+package gets admitted. Read this page alone to run the process — you do not
+need to read any ADR first.
+
+**What this page is not:** a history of *why* Compono's admission policy
+looks the way it does. That rationale — the alternatives considered, the
+research behind each threshold, the real candidates evaluated against
+it — lives in [ADR-0029](../adr/0029-milestone-7-dogfooding-strategy-and-capability-gap-decision-framework.md)
+and [ADR-0039](../adr/0039-future-extension-package-admission-gate-and-release-sequence.md)
+(see **Provenance** at the bottom). This page is kept in sync with those
+ADRs as the current process; if a future ADR changes the policy, this page
+must be updated in the same PR — the same "update docs alongside the
+decision that changed them" rule this repo already applies to every other
+subsystem doc (see [Contributing](../contributing.md)).
+
+## When this process applies
+
+Run this process for:
+
+- a genuinely new capability (Compono does something it doesn't do today)
+- a material expansion of an existing package's public surface
+- a new extension/integration package
+
+Do **not** run this process for:
+
+- a bug fix restoring behavior an `Accepted` ADR or existing documentation
+ already promised
+- straightforward implementation work against an already-`Accepted` ADR
+- a small, mechanical, additive change with no new design decision (a
+ cookbook recipe, a test, a docs fix)
+
+This process is a gate on new *design* decisions, not ceremony for every
+change — see [`contributing.md`](../contributing.md)'s "before you start"
+section for the same boundary stated from a contributor's side ("anything
+beyond a small fix... open a Feature Request issue first").
+
+## The two-stage model
+
+Every candidate passes through two independent gates, in order. They
+answer different questions and neither substitutes for the other:
+
+| Gate | Question | Answered by |
+|---|---|---|
+| **Gate A — Architectural admission** | Could this legitimately be part of Compono at all? | This page, applied once per candidate |
+| **Gate B — Evidence admission** | Is there real reason to build it *now*? | Real demand: dogfooding friction, a repeated consumer request, or an explicit product-owner request |
+
+A candidate that fails Gate A is rejected (or downgraded to a
+documentation-only idea) regardless of how much demand exists for it — Gate
+A is not a formality an eager candidate can outweigh with enthusiasm. A
+candidate that clears Gate A but has no Gate B evidence yet is a real,
+recorded idea, not yet worth building.
+
+### Terminology
+
+These four terms have precise, distinct meanings — use them, not looser
+synonyms, when discussing where a candidate stands:
+
+1. **Candidate** — proposed, not yet evaluated against Gate A.
+2. **Admitted candidate** — cleared Gate A: architecturally legitimate,
+ still no evidence. Recorded in
+ [`docs/roadmap/future-packages.md`](../roadmap/future-packages.md), not
+ as its own `Proposed` ADR.
+3. **Roadmap item** — cleared Gate B too: real evidence exists. Gets its
+ own problem-focused `Proposed` ADR, listed in
+ [`docs/roadmap/post-mvp.md`](../roadmap/post-mvp.md).
+4. **Committed implementation work** — the roadmap item's ADR reaches
+ `Accepted` (its own full design pass, not just the problem statement)
+ and a Plan moves `In Progress` against it.
+
+"Not a package" and "not a good idea" are different verdicts — a proposal
+can fail as a new *package* and still be admitted as core behavior, an
+addition to an existing package, or a documentation recipe. Don't conflate
+"this capability should exist" with "this deserves a new NuGet package";
+Gate A's last criterion below exists specifically to keep those questions
+separate.
+
+## Step-by-step: running the process
+
+### Step 1 — State the concrete problem
+
+Require a real consumer/testing/composition problem, described concretely
+— not "this would be nice" or "AutoFixture has this." Compono's explicit
+non-goal is reproducing AutoFixture feature-for-feature
+([design-principles.md](design-principles.md)); the existence of a feature
+somewhere else is not itself a reason to add it here. If you can't state a
+concrete scenario where a real test author is blocked or meaningfully
+worse off without this, stop — there's no problem yet to evaluate against
+the gates below.
+
+### Step 2 — Check how the problem is solved today
+
+Before treating this as a gap, check whether it's already solved by:
+
+- Compono core
+- an existing Compono extension package
+- ordinary .NET APIs
+- first-party Microsoft testing abstractions
+- an established ecosystem library
+- a few lines of ordinary consumer code
+- documentation/sample guidance (a Cookbook recipe, not a code change)
+
+The existence of boilerplate alone does not justify a new abstraction —
+Gate A's "meaningful abstraction" criterion (Step 4) asks specifically
+whether a consumer could already write this themselves in an afternoon.
+`Compono.FakeItEasy` failed admission for exactly this reason: FakeItEasy's
+`Sdk.Create.Fake(Type)` is a real extension point, but the resulting
+package would be ~80% structurally identical to the already-shipped
+`Compono.NSubstitute` — the "gap" was already closed by an existing
+package wearing a different ecosystem's name.
+
+**"The .NET API is short" answers a narrower question than "this is
+already solved."** Checking "is this already solved" means checking
+whether the consumer gets a natural, discoverable, composition-native
+answer *without leaving Compono's composition model* — not just whether
+the underlying framework call is short. A one-line `Options.Create(...)`
+or `new ConfigurationBuilder()...Build()` can still leave a real gap if a
+consumer has to repeatedly reconstruct integration ceremony by hand, keep
+several related registrations consistent with each other, or rediscover
+the same pattern from memory every time — that's friction in the
+composition workflow, not in the construction call, and this step must
+weigh both. Two real, `Accepted` precedents establish that Compono already
+treats this as legitimate: `CompositionBuilder.Share()`
+([ADR-0056](../adr/0056-composition-builder-share-graph-wide-sharing.md))
+was admitted even though `[Shared]` already made sharing fully possible,
+because expressing it as graph-wide, profile-reusable composition
+configuration — rather than an attribute a consumer must remember to
+attach to every relevant test signature — was itself real product value;
+`Compono.Bogus` ([ADR-0027](../adr/0027-compono-bogus-package-design.md))
+was admitted even though a consumer could always hand-write a
+plausible-looking fake value, because making that behavior discoverable
+and natural inside the composition model (`UseBogus()`) was the actual
+value, not raw difficulty. **Neither direction of this is a license to
+loosen the bar**: "the framework already lets you do this" does not, by
+itself, prove the capability is already well-solved *inside Compono's
+model* — but "I sometimes forget the exact syntax" does not, by itself,
+justify a Compono abstraction either. A convenience that leaves the
+consumer no more correct, expressive, reusable, or composition-native than
+before is still a trivial wrapper, and still fails Step 3 below — a named
+method wrapping one `Register()` call for an already-simple type is
+exactly this trap, not an example of legitimate ergonomic value.
+
+### Step 3 — Check Gate A: architectural admission
+
+A candidate must clear **all five** of the following — not just one — to
+become an admitted candidate. These are evaluated once per candidate, and
+none of them requires evidence of demand yet (that's Gate B):
+
+1. **Compono-specific value.** It solves meaningful composition-related
+ friction, not branding or convenience around an already-easy call. A
+ package that exists only because its underlying library is popular
+ fails here, even if it can technically claim to "supply composed
+ values." This friction is not only "the operation is hard to perform"
+ — real composition ergonomics count too, when the improvement is
+ genuinely about expressing intent inside Compono's composition model
+ rather than merely shortening syntax. Evidence worth weighing here
+ (none of it automatic — each still has to show up as a real, sourced
+ finding, not an assumption) includes: expressing intent at the
+ composition level instead of leaking it into individual tests or
+ production types; keeping consumers inside the composition model
+ instead of forcing them to reconstruct framework-specific ceremony
+ repeatedly; making behavior naturally reusable through profiles;
+ making related dependency shapes coherent instead of independently
+ hand-wired (and thus prone to silently drifting inconsistent);
+ establishing consistent semantics consumers would otherwise repeatedly
+ reinvent, each slightly differently; and making the obvious,
+ discoverable Compono path also the correct one. `Share()` and
+ `Compono.Bogus` (Step 2, above) both cleared this criterion on exactly
+ this kind of evidence, not on raw difficulty.
+2. **Native ecosystem fit.** The resulting API is idiomatic in the
+ integrated ecosystem's own terms — not a clone of an existing Compono
+ integration's shape bolted onto a different framework's extension
+ model. (`Compono.NUnit`'s `IParameterDataSource` gives genuine
+ per-parameter granularity `Compono.XunitV3`'s row model doesn't have —
+ a real, distinct shape, not a re-skin.)
+3. **Meaningful abstraction.** Consumers get materially more than a
+ trivial extension method they could write themselves in an afternoon.
+ Judge this against the *whole* consumer composition workflow, not just
+ the one construction expression — remembering which framework API/
+ package is involved, wrapping and registering it correctly, keeping
+ several related registrations consistent with each other, and making
+ the result reusable, can add up to real friction even when any single
+ line of it looks trivial in isolation. A candidate clears this
+ criterion by collapsing that workflow into a coherent composition
+ concept, not merely by giving an existing one-liner a Compono-branded
+ name — a helper that leaves the consumer no more correct, expressive,
+ reusable, or composition-native than before still fails here, no
+ matter how the friction is described.
+4. **Architectural fit.** It can be built entirely on an existing public
+ extension point, or on a to-be-designed extension point named
+ explicitly as a prerequisite — never on a core change invented ad hoc
+ during the candidate's own design pass. (`Compono`'s core package must
+ never reference or know about an integration package — see
+ [design-principles.md](design-principles.md)'s "Modular architecture."
+ If satisfying the candidate requires reflection or hidden state, that
+ conflicts with [ADR-0001](../adr/0001-source-generation-first.md)'s
+ no-reflection-by-default posture and needs a much higher bar to survive
+ as an intentional exception rather than a workaround.)
+5. **Package-boundary justification.** *If* this is proposed as a new
+ package: the dependency genuinely belongs outside core, and is
+ substantial enough to justify another independently-consumed artifact
+ rather than a documentation recipe or an addition to an existing
+ package. This is a separate question from "should this capability
+ exist" — see Step 6.
+
+Maintenance/CI/docs/skill-maintenance cost (an additional entry in the
+`compono` agent skill's detection table, another package guide, another
+CI package-validation target) is a real **weighing factor** across all
+five criteria above, not a standalone sixth pass/fail condition — it's
+linear and small per additional package for this repo's existing routing
+pattern, and shouldn't by itself veto a candidate that otherwise clears
+the five bars.
+
+A candidate that fails Gate A does not get a `Proposed` ADR of its own,
+regardless of demand. It's either rejected outright, or — where a Gate A
+finding surfaces a genuine, narrower recipe worth writing down —
+downgraded to a **documentation-only idea** recorded in
+`future-packages.md` (see Outcomes, below).
+
+### Step 4 — Check Gate B: evidence admission
+
+A candidate that clears Gate A is an admitted candidate — architecturally
+legitimate, but still just an idea until real evidence justifies building
+it now. Evidence can come from more than one source; dogfooding is strong
+evidence but is **not** the only accepted trigger:
+
+- **Dogfooding friction** — a real migration or real project surfaces
+ repeated, concrete friction. This is the strongest form of evidence
+ because it's falsifiable: a spike built to exercise a hypothesis is
+ structurally likely to "prove" it matters even when real usage wouldn't;
+ a real call site that already existed before Compono was involved is
+ not.
+- **A repeated, concrete consumer request** naming a specific scenario.
+- **An explicit product-owner request** — this alone has satisfied Gate B
+ for real, shipped packages (`Compono.TUnit`, `Compono.NUnit`, and
+ Compono-owned source-generated test doubles all cleared Gate B this way,
+ with no dogfooding evidence at the time). A clear ask from whoever owns
+ the product direction is real evidence, not a fallback used only when
+ dogfooding hasn't happened yet.
+
+When evidence does come from a real migration, weigh it with the same four
+questions every time — for the candidate gaps named below and any further
+one a migration surfaces:
+
+1. **Observed frequency.** How many real, distinct places actually needed
+ this behavior — not "could plausibly use it," but did, in the code as
+ it stood.
+2. **Was this scenario ever intended to work?** If Compono's documented or
+ `Accepted`-ADR behavior already claims to support the scenario and it
+ doesn't, that's a **bug**, not a design question — fix it through the
+ normal engineering workflow (`tasks/implement.md`/`tasks/pr-review.md`),
+ not this process. Skip the remaining questions.
+3. **Workaround cost.** Concretely, what does Compono's existing explicit
+ alternative cost — extra parameters, extra lines, an implementation
+ detail leaking into a test signature — shown as a real before/after, not
+ a hypothetical. A low or zero cost points toward "acceptable
+ alternative, no new capability needed"; a real, material cost points
+ toward a genuine capability gap.
+4. **Principle alignment.** Would satisfying this gap require reflection
+ or hidden state conflicting with the no-reflection-by-default posture,
+ or with this project's explicit-over-implicit bias
+ ([design-principles.md](design-principles.md))? A gap that can only be
+ closed by working against an existing constraint needs a much higher
+ bar on frequency and cost before it becomes a genuine capability gap
+ rather than an intentional design difference.
+
+**Do not turn "it must be dogfooded" into an absolute prerequisite** — an
+admitted candidate with no dogfooding history can still clear Gate B on an
+explicit, well-reasoned product-owner request. Conversely, don't let "a
+consumer might want this someday" stand in for real evidence either — a
+plausible-sounding future want is not evidence under any of the three
+triggers above.
+
+### Step 5 — Weigh the cost
+
+Evidence required should be proportional to the cost and permanence of what's
+being proposed. Consider, relative to the candidate's actual scope:
+
+- public API surface added
+- generator complexity, if any
+- runtime machinery and allocations
+- new dependencies
+- ongoing maintenance burden
+- documentation burden (a new Concept page, Package Guide, or Cookbook
+ entry)
+- skill/eval burden (the `compono` agent skill's per-package reference
+ files)
+- compatibility commitments and future design constraints this creates
+
+A one-line addition to an existing package's public surface needs far less
+evidence than a new package with its own release cadence and support
+surface.
+
+### Step 6 — If admitted, decide where it belongs
+
+Passing Gate A and Gate B means the capability should exist — it does not
+by itself mean it needs a new package. Work down this list and stop at the
+smallest home that's honestly justified:
+
+1. **Core `Compono`.** Only if it doesn't depend on any test framework or
+ test-double/data library — core must never reference or know about an
+ integration package.
+2. **An existing extension package.** If the capability's dependency
+ already matches an existing package's ecosystem (e.g. something
+ NSubstitute-specific belongs in `Compono.NSubstitute`, not a new
+ package).
+3. **A new extension package.** Only when Gate A's package-boundary
+ criterion is genuinely satisfied — the dependency belongs outside core
+ *and* outside every existing package, and is substantial enough to
+ justify its own independently-consumed artifact.
+4. **Documentation/sample guidance only.** When the capability is real and
+ worth recording, but doesn't need new code at all — a Cookbook recipe
+ showing how a consumer builds it themselves in a few lines.
+
+Two real precedents show this isn't a rubber stamp toward "new package":
+
+- **`Compono.DependencyInjection`** shipped, but as a narrower
+ configured-resolution `IServiceProvider` bridge
+ (`row.AsServiceProvider()`) than the "richer DI integration" idea
+ originally evaluated (keyed-service resolution, DI-scope ownership) —
+ that larger idea failed Gate A's architectural-fit criterion because it
+ needed a core concept that didn't exist yet, and remains a
+ documentation-only idea today, unrelated to the narrower thing that
+ actually shipped under the same name.
+- **FakeItEasy** support was downgraded from a package candidate to a
+ documentation-only recipe ("how to write your own
+ `ICompositionValueProvider` for FakeItEasy," following
+ `Compono.NSubstitute`'s published shape) rather than becoming
+ `Compono.FakeItEasy` — the capability is real and worth documenting, but
+ didn't justify its own package.
+
+## Outcomes
+
+Every evaluated candidate ends in exactly one of these:
+
+| Outcome | Meaning | Where it's recorded |
+|---|---|---|
+| **Rejected** | Fails Gate A; no legitimate Compono capability here | Not recorded as a candidate; reopen only if new evidence changes the Gate A analysis |
+| **Documentation-only** | Fails Gate A as a *package*, but the capability is worth a recipe/guide | `docs/roadmap/future-packages.md`, "Documentation-only ideas" |
+| **Deferred** | Clears Gate A, but blocked on an external factor (e.g. a dependency's maintenance health) | `docs/roadmap/future-packages.md`, "Deferred indefinitely," with an explicit re-evaluation trigger |
+| **Admitted candidate** | Clears Gate A; no Gate B evidence yet | `docs/roadmap/future-packages.md`, "Admitted candidates" |
+| **Roadmap item** | Clears Gate A and Gate B | Problem-only `Proposed` ADR, listed in `docs/roadmap/post-mvp.md` |
+| **Committed implementation work** | Roadmap item's ADR reaches `Accepted`, Plan moves `In Progress` | The ADR + its Plan, per the normal design/implement workflow |
+
+A finding from a real migration that isn't a genuine capability gap at all
+still gets classified and recorded (per ADR-0029), even though it never
+enters this table as a candidate:
+
+- **Acceptable Compono-native alternative** — a different API than the
+ thing being compared against, but the replacement stays pleasant (low
+ workaround cost, no material readability loss). Documented as a pattern
+ in the relevant guide; no ADR or Amendment needed.
+- **Intentional design difference** — the alternative would conflict with
+ Compono's principles, or costs more than its observed value justifies. A
+ dated Amendment to the ADR that governs the existing behavior records
+ the evidence and the "no change" verdict — this is a real, indexed "no,"
+ not a dropped finding.
+- **Migration-only friction** — pain during a one-time conversion that
+ doesn't persist in the resulting test suite. Recorded as a tip for the
+ next migrator; no ADR or Amendment needed.
+
+## Decision flow
+
+```
+Proposed capability / package
+ |
+ v
+Step 1: Is there a real, concrete problem?
+ | no -> stop, not a candidate
+ v yes
+Step 2: Is an existing solution already good enough?
+ | yes -> Documentation-only (a recipe, not a package)
+ v no
+Step 3: Gate A — architecturally legitimate?
+ (Compono-specific value, native ecosystem fit, meaningful
+ abstraction, architectural fit, package-boundary justification)
+ | fails -> Rejected, or Documentation-only if a narrower
+ | recipe genuinely survives
+ v clears
+ Admitted candidate
+ |
+ v
+Step 4: Gate B — real evidence now?
+ (dogfooding friction / repeated consumer request /
+ explicit product-owner request)
+ | no evidence yet -> stays Admitted candidate
+ | external blocker -> Deferred (with re-evaluation trigger)
+ v evidence exists
+ Roadmap item -> Proposed ADR
+ |
+ v
+ ADR reaches Accepted, Plan In Progress
+ |
+ v
+ Committed implementation work
+ |
+ v
+Step 6: Where does it live?
+ Core / Existing package / New package / Docs-only
+```
+
+## Worked examples
+
+Real Compono history, not hypotheticals:
+
+- **`Compono.TUnit` (admitted, shipped).** Cleared Gate A on a real,
+ distinct integration surface (TUnit's `IDataSourceAttribute` family,
+ per-row `TestBuilderContext`) — not because TUnit is source-generated
+ like Compono itself; that original rationale didn't survive scrutiny and
+ was explicitly retired. Cleared Gate B via an explicit product-owner
+ request, not dogfooding. Reached committed implementation work and
+ shipped.
+- **`Compono.Http` (admitted, shipped).** Cleared Gate B through real
+ dogfooding evidence: a real consumer project's hand-rolled,
+ reflection-based `HttpMessageHandler` fake, used across 41 real call
+ sites, with duplicated fake-handler classes solving the same problem
+ three different ways. The friction was real, repeated, and worse than
+ every surveyed alternative — a strong Gate B case.
+- **`Compono.FakeItEasy` (rejected as a package, documentation-only).**
+ Real extension point, but ~80% structurally identical to
+ `Compono.NSubstitute` — failed "meaningful abstraction" relative to a
+ package that already exists, not because FakeItEasy's own API is thin.
+- **`CompositionBuilder.Share()` (admitted, shipped — composition
+ ergonomics, not raw difficulty).** `[Shared]` already made sharing a
+ value across a composition graph fully possible before this shipped —
+ nothing was technically blocked. Admitted anyway because expressing
+ sharing as graph-wide, profile-reusable composition configuration,
+ instead of an attribute a consumer has to remember to attach to every
+ relevant test signature, was itself real Compono-specific value (Step 2/
+ Gate A criterion 1, above).
+- **`Compono.Bogus` (admitted, shipped — composition ergonomics, not raw
+ difficulty).** A consumer could always hand-write a plausible-looking
+ fake value; nothing about that is hard. Admitted because making
+ realistic-looking values discoverable and natural inside the
+ composition model (`UseBogus()`) was the actual value.
+- **`Compono.Moq` (deferred).** A workable integration surface exists, but
+ Moq had shipped no release in roughly 23 months and carries
+ reputational damage from a past incident — deferred with an explicit
+ re-evaluation trigger (Moq resumes active releases), not silently
+ dropped.
+- **NSubstitute's `ConfigureMembers` (intentional design difference).**
+ Real dogfooding surfaced a case where AutoFixture's
+ `AutoNSubstituteCustomization { ConfigureMembers = true }`
+ auto-configures every generated substitute's members recursively.
+ `Compono.NSubstitute` deliberately doesn't — that gap was weighed
+ through the Gate B rubric and, if the evidence supports it, recorded as
+ a dated Amendment to the ADR governing that decision rather than
+ becoming a new roadmap item. (Illustrative of the *mechanism*; check
+ that ADR's own Amendments for the actual, current verdict rather than
+ treating this summary as the record.)
+
+## When this page is not enough
+
+This page is the operational summary. For the full reasoning behind a
+threshold, a rejected alternative, or a specific candidate's disposition,
+follow the links into the ADRs below — you shouldn't need to, but they're
+the permanent record if you want it.
+
+## Provenance
+
+This page consolidates the current admission policy from:
+
+- [ADR-0029](../adr/0029-milestone-7-dogfooding-strategy-and-capability-gap-decision-framework.md)
+ — the evidence rubric (Gate B), the four evidence questions, the
+ five-way finding classification (bug / roadmap candidate / acceptable
+ alternative / intentional design difference / migration-only friction),
+ and the bug-handling carve-out.
+- [ADR-0039](../adr/0039-future-extension-package-admission-gate-and-release-sequence.md)
+ and its Amendment 1 — the two-stage model, Gate A's five criteria, the
+ candidate/admitted-candidate/roadmap-item/committed-implementation-work
+ terminology, and the explicit rejection of a committed release sequence.
+- Real Gate A/Gate B applications recorded in
+ [ADR-0040](../adr/0040-compono-tunit-package-design.md) (`Compono.TUnit`),
+ [ADR-0042](../adr/0042-compono-owned-source-generated-test-doubles.md)
+ (Compono-owned test doubles),
+ [ADR-0051](../adr/0051-compono-http-handler-based-testing-package.md)
+ (`Compono.Http`), and
+ [ADR-0059](../adr/0059-compono-nunit-package-design.md) (`Compono.NUnit`)
+ — the worked examples above are drawn from these.
+- [`docs/roadmap/future-packages.md`](../roadmap/future-packages.md) and
+ [`docs/roadmap/post-mvp.md`](../roadmap/post-mvp.md) — the live,
+ current status of every candidate this process has ever evaluated.
+- [ADR-0056](../adr/0056-composition-builder-share-graph-wide-sharing.md)
+ and [ADR-0027](../adr/0027-compono-bogus-package-design.md) — the
+ `Share()`/`Compono.Bogus` precedent establishing that composition
+ ergonomics (Step 2, Gate A criterion 1 above) count as legitimate
+ Compono-specific value independent of raw operation difficulty.
+- [`docs/research/0028-compono-options-configuration-admission-research.md`](../research/0028-compono-options-configuration-admission-research.md)
+ — the investigation whose reassessment surfaced that this page could be
+ read too narrowly (raw API simplicity treated as sufficient reason to
+ stop at Step 2) and fed the composition-ergonomics clarifications above
+ back into this page.
+
+The ADRs above remain authoritative for historical rationale and the
+architectural decision history — the alternatives considered, why they
+were rejected, and the research behind each threshold. **This page is the
+canonical current operational description of the admission process.**
+When a future ADR changes the policy (a new Gate A criterion, a revised
+evidence bar, a retired outcome category), update this page in the same
+PR — an admission process that only lives correctly in an ADR's prose
+recreates exactly the discoverability problem this page exists to solve.
diff --git a/docs/architecture/index.md b/docs/architecture/index.md
index e5f02aec..19a673f1 100644
--- a/docs/architecture/index.md
+++ b/docs/architecture/index.md
@@ -15,6 +15,10 @@ underlying decision rather than re-deriving the reasoning.
- **[Design Principles](design-principles.md)** — current, evolving: what
Compono believes (composition over object generation, predictability
over magic, source-generated by default, deterministic by design).
+- **[Capability & Package Admission](capability-admission.md)** — the
+ current, standalone process for deciding whether a proposed capability,
+ feature, integration, or package gets admitted into Compono. Read this
+ before proposing new scope, not the ADRs it's consolidated from.
- **Current Architecture** — how it works today:
[Source Generation](current/source-generation.md),
[Generated Plans and Discovery](current/generated-plans-and-discovery.md),
diff --git a/docs/contributing.md b/docs/contributing.md
index bde0642a..88ffaa48 100644
--- a/docs/contributing.md
+++ b/docs/contributing.md
@@ -22,7 +22,12 @@ instead of opening a public issue.
issue first and let a maintainer weigh in before you invest in an
implementation — this repo intentionally has one way to do each thing,
and a design conversation up front is cheaper than a large PR that has
- to change direction in review.
+ to change direction in review. If what you're proposing is a new
+ capability or package rather than a fix, run it through
+ [Capability & Package Admission](architecture/capability-admission.md)
+ first — it's the standalone process Compono uses to decide whether
+ something like this belongs in the project at all, before any design
+ work starts.
- Looking for a first contribution? A missing [Cookbook](cookbook/index.md)
recipe is the easiest way in — narrow in scope, easy to review, and
doesn't require touching the composition engine itself. See the
diff --git a/docs/cookbook/compose-configuration-from-an-in-memory-collection.md b/docs/cookbook/compose-configuration-from-an-in-memory-collection.md
new file mode 100644
index 00000000..0ea40372
--- /dev/null
+++ b/docs/cookbook/compose-configuration-from-an-in-memory-collection.md
@@ -0,0 +1,84 @@
+---
+title: Compose Configuration From an In-Memory Collection
+description: Register a real IConfiguration for a test, built from plain in-memory key/value pairs.
+packages: [Compono]
+concepts: [registration]
+---
+
+# Compose Configuration From an In-Memory Collection
+
+## Problem
+
+Your code under test depends on `IConfiguration` directly (`GetSection`,
+`GetValue`, configuration-binding helpers) and the test wants a real,
+predictable `IConfiguration` instance — not a hand-rolled fake, and no
+external config file on disk.
+
+## Solution
+
+```csharp
+using Microsoft.Extensions.Configuration;
+
+IConfiguration BuildConfiguration() =>
+ new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ ["Database:ConnectionString"] = "test-connection-string",
+ ["Database:TimeoutSeconds"] = "5",
+ })
+ .Build();
+```
+
+```csharp
+public sealed class DatabaseConfigurationProfile : ICompositionProfile
+{
+ public void Configure(CompositionBuilder builder) =>
+ builder.Register(BuildConfiguration);
+}
+```
+
+```csharp
+[Theory]
+[Compose]
+public void ReadsTheConnectionStringFromConfiguration(IConfiguration configuration)
+{
+ configuration["Database:ConnectionString"].Should().Be("test-connection-string");
+}
+```
+
+Or, wired directly through a hand-built `Composer` without a test-framework
+attribute at all:
+
+```csharp
+var composer = Composer.Create(builder => builder
+ .Register(BuildConfiguration));
+
+var repository = composer.Create();
+```
+
+## Discussion
+
+This is ordinary `Microsoft.Extensions.Configuration` composition —
+`Compono` adds nothing beyond the standard `Register` call.
+`AddInMemoryCollection` accepts colon-separated keys (`"Database:TimeoutSeconds"`)
+the same way a real `appsettings.json`'s nested sections flatten when
+bound, so `configuration.GetSection("Database").GetValue("TimeoutSeconds")`
+and configuration-binding (`configuration.GetSection("Database").Get()`)
+both work exactly as they would against a real configuration source.
+
+There is no `Compono.Configuration` package — plain
+`ConfigurationBuilder`/`Register` composition, as above, is
+the whole answer for `IConfiguration` itself. If your code under test
+instead depends on `IOptions`/`IOptionsSnapshot`/`IOptionsMonitor`
+for a strongly-typed settings class, that's
+[`Compono.Options`](../packages/compono-options.md)'s job, not this
+recipe's — the two compose independently and don't need to agree with each
+other's values.
+
+## See also
+
+- [Layer Configuration Overrides in a Test](layer-configuration-overrides-in-a-test.md)
+- [Reuse Configuration Through a Profile](reuse-configuration-through-a-profile.md)
+- [`Compono.Options` Package Guide](../packages/compono-options.md) — for
+ `IOptions`/`IOptionsSnapshot`/`IOptionsMonitor` instead of plain
+ `IConfiguration`.
diff --git a/docs/cookbook/index.md b/docs/cookbook/index.md
index bbf9fa05..6fd33751 100644
--- a/docs/cookbook/index.md
+++ b/docs/cookbook/index.md
@@ -16,9 +16,12 @@ made once the list is actually large enough to need it, not guessed at now.
## Recipes
- [Compose a Substitute With One Method Stubbed](compose-a-substitute-with-one-method-stubbed.md)
+- [Compose Configuration From an In-Memory Collection](compose-configuration-from-an-in-memory-collection.md)
- [Freeze a Shared HttpMessageHandler](freeze-a-shared-httpmessagehandler.md)
- [Generate a Realistic Email](generate-a-realistic-email.md)
+- [Layer Configuration Overrides in a Test](layer-configuration-overrides-in-a-test.md)
- [Override One Field Only for One Test](override-one-field-only-for-one-test.md)
+- [Reuse Configuration Through a Profile](reuse-configuration-through-a-profile.md)
- [Seed a Specific Failing Case for Reproduction](seed-a-specific-failing-case-for-reproduction.md)
## Next
diff --git a/docs/cookbook/layer-configuration-overrides-in-a-test.md b/docs/cookbook/layer-configuration-overrides-in-a-test.md
new file mode 100644
index 00000000..740dd1bb
--- /dev/null
+++ b/docs/cookbook/layer-configuration-overrides-in-a-test.md
@@ -0,0 +1,66 @@
+---
+title: Layer Configuration Overrides in a Test
+description: Start from a shared baseline configuration and override just the values one test cares about.
+packages: [Compono]
+concepts: [registration]
+---
+
+# Layer Configuration Overrides in a Test
+
+## Problem
+
+Most tests should share one baseline configuration, but a specific test
+needs one or two values different — without duplicating every other key
+just to change one.
+
+## Solution
+
+```csharp
+using Microsoft.Extensions.Configuration;
+
+IConfiguration BuildConfiguration(IDictionary? overrides = null)
+{
+ var builder = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ ["Retry:MaxAttempts"] = "3",
+ ["Retry:DelayMilliseconds"] = "100",
+ });
+
+ if (overrides is not null)
+ {
+ builder.AddInMemoryCollection(overrides);
+ }
+
+ return builder.Build();
+}
+```
+
+```csharp
+var composer = Composer.Create(builder => builder
+ .Register(() => BuildConfiguration(
+ new Dictionary { ["Retry:MaxAttempts"] = "0" })));
+
+var client = composer.Create();
+// client observes Retry:MaxAttempts = "0", Retry:DelayMilliseconds = "100" (unchanged baseline)
+```
+
+## Discussion
+
+`AddInMemoryCollection`, like every `IConfigurationSource`, follows
+`Microsoft.Extensions.Configuration`'s own later-source-wins layering — a
+later `AddInMemoryCollection` call overrides a key an earlier one already
+set, and leaves every key it doesn't mention untouched. This is real
+`IConfiguration` behavior, not a Compono-specific mechanism; the same
+pattern applies to `AddJsonFile`/`AddEnvironmentVariables`/any other real
+source if a project's test setup needs them.
+
+Keep the override dictionary scoped to exactly the keys one test needs to
+differ — resist the temptation to duplicate the whole baseline "just to be
+safe." A test that overrides one key should be legible as "the same
+configuration as everywhere else, except this one thing."
+
+## See also
+
+- [Compose Configuration From an In-Memory Collection](compose-configuration-from-an-in-memory-collection.md)
+- [Reuse Configuration Through a Profile](reuse-configuration-through-a-profile.md)
diff --git a/docs/cookbook/reuse-configuration-through-a-profile.md b/docs/cookbook/reuse-configuration-through-a-profile.md
new file mode 100644
index 00000000..5521bf09
--- /dev/null
+++ b/docs/cookbook/reuse-configuration-through-a-profile.md
@@ -0,0 +1,90 @@
+---
+title: Reuse Configuration Through a Profile
+description: Establish one project's baseline IConfiguration once, in a shared ICompositionProfile.
+packages: [Compono]
+concepts: [profiles, registration]
+---
+
+# Reuse Configuration Through a Profile
+
+## Problem
+
+Every test in a project needs the same baseline `IConfiguration` — copying
+the same `ConfigurationBuilder`/`AddInMemoryCollection` call into every
+test class is repetitive and drifts inconsistent over time.
+
+## Solution
+
+```csharp
+using Microsoft.Extensions.Configuration;
+
+public sealed class AppConfigurationProfile : ICompositionProfile
+{
+ public void Configure(CompositionBuilder builder) => builder
+ .Register(() => new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ ["Retry:MaxAttempts"] = "3",
+ ["Retry:DelayMilliseconds"] = "100",
+ })
+ .Build());
+}
+```
+
+```csharp
+[Theory]
+[Compose]
+public void ReadsTheSharedBaselineConfiguration(IConfiguration configuration)
+{
+ configuration["Retry:MaxAttempts"].Should().Be("3");
+}
+```
+
+Or applied to a hand-built `Composer` directly:
+
+```csharp
+var composer = Composer.Create(builder => builder.AddProfile());
+```
+
+## Discussion
+
+`ICompositionProfile.Configure` is just an ordinary sequence of
+`CompositionBuilder` calls — `Register` needs no special
+profile-only mechanism to be reusable this way, the same as any other
+registration.
+
+Registering the same exact type more than once is a build-time conflict in
+Compono, not last-write-wins or first-write-wins (`docs/adr/0019-registrations-and-service-provider-injection.md`) —
+so a test can't compose `AppConfigurationProfile` (which already calls
+`Register`) and *then* call `Register`
+again itself to layer an override; that collides and throws
+`CompositionConfigurationException`. A test that needs the shared baseline
+plus one local override instead passes the override into the profile's own
+constructor and applies it with the instance-based
+`AddProfile(ICompositionProfile)` overload, so the profile itself is the
+one and only place that calls `Register`:
+
+```csharp
+public sealed class AppConfigurationProfile(IDictionary? overrides = null) : ICompositionProfile
+{
+ public void Configure(CompositionBuilder builder) => builder
+ .Register(() => BuildConfiguration(overrides));
+}
+```
+
+```csharp
+var composer = Composer.Create(builder => builder.AddProfile(
+ new AppConfigurationProfile(new Dictionary { ["Retry:MaxAttempts"] = "0" })));
+```
+
+See [Layer Configuration Overrides in a Test](layer-configuration-overrides-in-a-test.md)
+for `BuildConfiguration`'s own layering (multiple `AddInMemoryCollection`
+calls inside one `ConfigurationBuilder`, not multiple `Register` calls),
+and [Composition Profiles](../concepts/profiles.md) for the full profile
+mechanics.
+
+## See also
+
+- [Compose Configuration From an In-Memory Collection](compose-configuration-from-an-in-memory-collection.md)
+- [Layer Configuration Overrides in a Test](layer-configuration-overrides-in-a-test.md)
+- [Composition Profiles](../concepts/profiles.md)
diff --git a/docs/index.md b/docs/index.md
index 0526f10a..7f010a22 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -47,6 +47,7 @@ Compono determines **how** those requirements are satisfied.
| `Compono.Logging` | `Microsoft.Extensions.Logging` testing support (`UseLogging()`, `CapturingLogger