[ADR-0027] Compono.Bogus Package Design¶
Status: Accepted
Date: 2026-07-31
Decision Makers: Nick Cipollina, Claude (design review)
Context¶
ADR-0024 settled the mechanism a stage-⅚ package plugs into, and validated (by sketch, not commitment) that it would hold up for Bogus. ADR-0026 closed the one real gap that sketch exposed: nothing gave a provider or factory deterministic randomness. This ADR turns docs/mvp.md's Milestone 6 scope ("Semantic value-provider contract, Shared deterministic seed, Bogus Faker access, Locale configuration, Conservative member-name conventions, Explicit member rules, Initial correlated-value experiment") into a real design, and corrects docs/public-api.md's existing Bogus Integration sketch, which predates both ADR-0024 and ADR-0026 and shows a context.Semantic.Email() accessor that would require core ICompositionContext to carry Bogus-shaped vocabulary — ruled out during this milestone's design review (design-decisions.md rule 3).
Compono.Bogus ends up with three distinct, independently useful customization models, not one:
- A built-in, conservative member-name convention provider (stage 5) — the "just call
UseBogus()and get realistic values" default case. - An explicit member-level rule —
.For<T>().Member(x => x.Y).UseBogus(faker => ...)(stage 4) — for a specific member the conventions don't (or shouldn't) guess. - An explicit whole-object
Faker<T>registration —UseBogus<T>(faker => faker.RuleFor(...))(compiles to stage 3) — for a type a consumer wants Bogus to construct entirely, correlated rules and all, instead of the generated constructor plan.
All three share one deterministic-randomness story (DeriveSeed(), ADR-0026) and one coexistence story with Compono.NSubstitute (disjoint type claims — see below) — this ADR designs all three together since they're one package's public surface, but keeps them conceptually separate rather than forcing them into a single mechanism.
Decision Drivers¶
docs/mvp.md's explicit caution: "Ambiguous member names such asNameshould not be guessed aggressively" — the convention provider's allowlist must be small, exact-match, and type-checked, never fuzzy/substring matching.docs/mvp.md's MVP success criterion #7 /design-decisions.mdrule 3: the coreComponopackage must never reference or know about Bogus. Every design choice below routes through core capabilities that already exist (stage 5 providers, stage 4 rules, stage 3 registrations,DeriveSeed()) rather than asking core for anything Bogus-shaped.- Coexistence with
Compono.NSubstitute, without either package depending on the other:docs/architecture.md's Resolution Pipeline already fixes stage 5 (semantic) before stage 6 (test-double), and a request each package's provider can plausibly both want to claim must not arise. This ADR achieves that by construction — see Coexistence withCompono.NSubstitute, below — not by either provider special-casing the other. - Determinism (ADR-0026): every Bogus-produced value must be reproducible from the same root seed, independent of unrelated members added elsewhere in the graph, and safe under concurrent composition (no shared mutable
Faker/Randomizerstate across requests). docs/mvp.md's explicit non-goal framing (mirroring ADR-0025's own "no recursive auto-configuration"/"no framework API wrappers" restraint): this package activates and configures Bogus; it does not re-expose or wrap Bogus's ownFaker/Faker<T>API surface beyond what activation requires.
Considered Options¶
Whole-object correlated values: .DependsOn(...) vs. Faker<T>¶
- Design a new Compono-native member-dependency mechanism —
.For<T>().Member(x => x.Email).DependsOn(x => x.FirstName, x => x.LastName).UseBogus((faker, first, last) => ...), requiring new core semantics: member composition ordering, retaining already-composed sibling values, dependency-cycle detection, and interaction with constructor-parameter/required-member composition order. - Use Bogus's own
Faker<T>for the correlated case, which already threads the partially-populated object into a laterRuleForcallback (.RuleFor(x => x.Email, (f, x) => f.Internet.Email(x.FirstName, x.LastName))) — no new Compono mechanism at all, sinceFaker<T>already solves exactly this problem inside Bogus itself.
Whole-object Faker<T> registration: new pipeline concept vs. sugar over Register<T>¶
- A first-class
UseBogus<T>(...)API that is purely ergonomic sugar over the existing stage-3Register<T>(Func<ICompositionContext, T>)registration mechanism, with no hidden pipeline stage and no special runtime behavior of its own — no new pipeline stage, no new conflict-detection rule; duplicate registration for the sameTis the same build-timeCompositionConfigurationExceptionRegister<T>already produces. - Document the raw
Register<T>(context => faker.UseSeed(context.DeriveSeed()).Generate())pattern only, with no package-owned convenience API at all.
Locale coupling between package-wide activation and UseBogus<T>()¶
- Fully independent —
UseBogus<T>()takes its own optional locale (or defaults to Bogus's own default), with no dependency on whether/howUseBogus()/UseBogus(options => ...)was called elsewhere in the same configuration. UseBogus<T>()requires priorUseBogus()activation and reads the sharedBogusOptions.Locale, making the two calls order-dependent.
Decision Outcome¶
Chosen: Option 2 (Faker<T> for correlated values, not .DependsOn(...)), Option 1 (UseBogus<T>() as sugar over Register<T>), Option 1 (fully independent locale) — all three confirmed directly with the user during design review.
When to reach for each model — the three are complementary, not competing, and a real profile typically uses more than one at once (see docs/public-api.md's Bogus Integration section for the worked example):
UseBogus()— project-wide conventions: "mostFirstName/Email/etc. members across the whole graph should just look realistic," with zero per-type setup..Member(...).UseBogus(faker => ...)— a handful of members need something the convention allowlist doesn't (or shouldn't) guess — an ambiguous name, a domain-specific format, a member the convention provider would otherwise miss entirely.UseBogus<T>()— a type's values are meaningfully correlated with each other (an email derived from a name, a full address whose fields agree) and the whole object is more naturally described as "Bogus owns this type" than as several independent member rules.
Model 1: Conservative member-name conventions (stage 5)¶
public sealed class BogusOptions
{
/// <summary>
/// The Bogus locale used by the package-wide member-name convention provider
/// (<see cref="BogusMemberNameProvider"/>) only. <c>UseBogus<T>()</c> is independent of this
/// option and does not read it — it defaults to <c>"en"</c> on its own, or takes an explicit
/// <c>locale</c> parameter. Defaults to Bogus's own default ("en").
/// </summary>
public string Locale { get; set; } = "en";
/// <summary>Whether the conservative member-name convention provider is active. Defaults to <see langword="true"/>.</summary>
public bool EnableMemberNameConventions { get; set; } = true;
}
public sealed class BogusMemberNameProvider : ICompositionValueProvider
{
private readonly string _locale;
public BogusMemberNameProvider(string locale) => _locale = locale;
public CompositionProviderResult TryProvide(in CompositionProviderRequest request, ICompositionContext context)
{
if (request.RequestedType != typeof(string) || request.Name is not { } name)
return CompositionProviderResult.NotHandled;
if (!Conventions.TryGetValue(name, out var generate))
return CompositionProviderResult.NotHandled;
var faker = new Faker(_locale) { Random = new Randomizer(context.DeriveSeed()) };
return CompositionProviderResult.Handled(generate(faker));
}
// Immutable, built once — a FrozenDictionary, not a plain Dictionary, since this is a
// fixed, read-only lookup table shared across every request this provider ever handles.
private static readonly FrozenDictionary<string, Func<Faker, string>> Conventions =
new Dictionary<string, Func<Faker, string>>
{
["FirstName"] = f => f.Name.FirstName(),
["LastName"] = f => f.Name.LastName(),
["FullName"] = f => f.Name.FullName(),
["Email"] = f => f.Internet.Email(),
["PhoneNumber"] = f => f.Phone.PhoneNumber(),
["StreetAddress"] = f => f.Address.StreetAddress(),
["City"] = f => f.Address.City(),
["State"] = f => f.Address.State(),
["PostalCode"] = f => f.Address.ZipCode(),
["CompanyName"] = f => f.Company.CompanyName(),
}.ToFrozenDictionary();
}
- Exact match only, case-sensitive, against
CompositionProviderRequest.Name— no substring/prefix/fuzzy matching, no attempt at pluralization or synonym handling.Name(ambiguous perdocs/mvp.md's own callout) is deliberately absent from the allowlist. - Type-gated to
stringonly —RequestedType != typeof(string)declines immediately. This is also what makes coexistence withCompono.NSubstituteautomatic (below): every convention indocs/mvp.md's list produces astring, so this provider can never claim an interface, delegate, or abstract class — the exact request shapesNSubstituteProviderclaims instead. - On by default whenever
UseBogus()/UseBogus(options => ...)is called — matchingCompono.NSubstitute's own "substitutable by default, only the narrower abstract-class case needs its own toggle" precedent. A consumer who wants Bogus active only for explicit rules setsoptions.EnableMemberNameConventions = false. - A fresh
Faker/Randomizerper handled request, seeded fromcontext.DeriveSeed()— never a package-lifetime-shared instance. This is what keeps every produced value both deterministic (same seed, same request path → same value) and safe under concurrent composition (no instance is ever touched from more than one request). - Yields to everything earlier in the pipeline — stage 3 (registrations), stage 4 (configuration rules, including Model 2's own member-level
UseBogus(...)rule) both run before stage 5, so an explicit rule for the same member always wins over the convention guess, with zero special-casing in this provider — purely a consequence ofdocs/architecture.md's already-fixed stage order.
Model 2: Explicit member-level rule (stage 4, sugar over .Use(...))¶
namespace Compono;
public static class MemberRuleExtensions
{
extension<T, TMember>(MemberRuleBuilder<T, TMember> builder)
where TMember : notnull
{
public CompositionBuilder UseBogus(Func<Faker, TMember> configure, string locale = "en")
{
ArgumentNullException.ThrowIfNull(configure);
return builder.Use(context =>
{
var faker = new Faker(locale) { Random = new Randomizer(context.DeriveSeed()) };
return configure(faker);
});
}
}
}
(Exact generic member-rule-builder type name — MemberRuleBuilder<T, TMember> above — is illustrative, matching whatever ADR-0020's .For<T>().Member(x => x.Y) chain actually returns; not a new type this ADR introduces.)
- No
context.Semanticaccessor, no core change beyond ADR-0026's already-genericDeriveSeed().Compono.BogusownsFakerconstruction, locale, andRandomizerseeding entirely on its own side of the.Use(context => ...)boundary ADR-0020 already established for every other stage-4 rule. - Compiles into an ordinary stage-4 rule, so it automatically wins over Model 1's convention guess for the same member (member rules already take precedence over type rules and, by pipeline order, over stage 5 entirely) and is itself overridden by an exact stage-3 registration for the same type, exactly like any other rule.
- Takes its own
localeparameter (default"en", matching Bogus's own default) — deliberately independent ofBogusOptions.Locale, consistent with Model 3's own locale independence, below.
Model 3: Whole-object Faker<T> registration (compiles to stage 3)¶
namespace Compono;
public static class CompositionBuilderExtensions
{
extension(CompositionBuilder builder)
{
public CompositionBuilder UseBogus() => builder.UseBogus(static _ => { });
public CompositionBuilder UseBogus(Action<BogusOptions> configure)
{
ArgumentNullException.ThrowIfNull(configure);
var options = new BogusOptions();
configure(options);
if (options.EnableMemberNameConventions)
builder.AddSemanticProvider(new BogusMemberNameProvider(options.Locale));
return builder;
}
public CompositionBuilder UseBogus<T>(Action<Faker<T>> configureFaker)
where T : class =>
builder.UseBogus("en", configureFaker);
public CompositionBuilder UseBogus<T>(string locale, Action<Faker<T>> configureFaker)
where T : class
{
ArgumentNullException.ThrowIfNull(configureFaker);
return builder.Register<T>(context =>
{
var faker = new Faker<T>(locale);
configureFaker(faker);
return faker.UseSeed(context.DeriveSeed()).Generate();
});
}
}
}
- Fully independent of
UseBogus()/UseBogus(options => ...)— no ordering dependency, no implicit read ofBogusOptions.Locale. A consumer who wants both to share a locale centralizes the string themselves (a profile constant, per the design-review example), rather than this package inferring it through hidden coupling. This is deliberate, not an oversight worth "fixing" with shared state later:UseBogus<T>()is purely ergonomic sugar over the existingRegister<T>()registration mechanism (stage 3) — a plain exact registration, no hidden pipeline stage, no special runtime behavior of its own — whileUseBogus()activatesBogusMemberNameProvider(stage 5). They're different pipeline stages solving different problems, and neither needs the other to function; a consumer can callUseBogus<T>()alone, withUseBogus()never called at all, and it works exactly the same. Coupling them (requiring one before the other, or silently reading shared options) would blur that separation for a benefit that only shows up when a consumer wants the same locale in both places — handled instead by that consumer sharing one string constant, per the worked example indocs/public-api.md's Bogus Integration section. localeis a plainstring, not an options type, and this is deliberate. ABogusGenerationOptions-style type was considered during review and rejected:Localeis the only per-registration settingFaker<T>'s own constructor takes today, anddocs/public-api.md's own API Design Rules already caution against speculative surface — a one-property options type exists only to look extensible, not because a second option is needed now. If a real per-registration knob emerges (Milestone 7 dogfooding, or later), anAction<BogusGenerationOptions>-based overload can be added then, alongside the existingstring-based one, without breaking it — this ADR's Alpha Compatibility Policy (inherited from ADR-0024) already covers exactly this kind of justified, evidence-driven addition. Call sites should use the named argument for readability:UseBogus<Customer>(locale: "fr", configureFaker: faker => faker.RuleFor(...)).configureFakerisAction<Faker<T>>, notFunc<Faker<T>, Faker<T>>. The callback configures aFaker<T>instance; it doesn't transform one into another.Faker<T>'s ownRuleFor/CustomInstantiator/FinishWithAPI is fluent and returnsthispurely as a chaining convenience — aFunc-shaped callback would only ever accidentally work if a caller forgot to return the same instance (or deliberately returned a different one, silently discarding the original).Action<Faker<T>>makes returning anything impossible to get wrong, and matches this package's ownAction<BogusOptions>/Action<NSubstituteOptions>-shaped configuration callbacks elsewhere in Compono, rather than introducing the oneFunc-shaped configuration callback in the whole public surface.- Compiles to
Register<T>(Func<ICompositionContext, T>)— no new pipeline stage, no new conflict rule. TwoUseBogus<T>()calls (or oneUseBogus<T>()plus one directRegister<T>(...)) for the sameThit the exact same build-timeCompositionConfigurationExceptionany duplicate registration already produces (ADR-0019). - A fresh
Faker<T>per resolved request, never a shared instance.configureFaker(theAction<Faker<T>>callback) is captured once atBuild()time, same as any otherRegister<T>factory closure — but it is invoked once perTresolution, and each invocation constructs its own newFaker<T>(locale)before handing it toconfigureFaker. There is noFaker<T>instance alive before the first request, and no instance is retained or reused after a request completes:context.DeriveSeed()(this specific request's own derived seed) andGenerate()both happen against that request's own fresh instance, inside the sameRegister<T>factory call. This matchesNSubstituteProvider's own "stateless per call" shape exactly — no mutable state survives from one request to the next, so two concurrentCreate<T>()calls for the sameTnever touch the sameFaker<T>object at all, let alone race on it.
Caching a configured Faker<T> across requests was considered and deliberately rejected. The obvious "optimization" — construct one Faker<T>, run configureFaker on it once at Build() time, and reuse that same instance on every subsequent Generate() call, reseeding it per request via UseSeed(...) — looks cheaper (one Faker<T> construction instead of one per request) but is unsafe: Faker<T> carries mutable generation state (its own internal Randomizer, rule-evaluation state UseSeed/Generate read and write), and Bogus does not document or guarantee that a single Faker<T> instance tolerates concurrent Generate() calls. Reusing one instance across requests would reintroduce exactly the shared-mutable-state race this section's "never a shared instance" design avoids, the first time someone reached for it as a performance improvement. This section's design — a fresh instance per request — is the version to keep; a future contributor proposing the cached-instance shape as a same-behavior optimization should treat this paragraph as the record of why it was already rejected, not rediscover the tradeoff from scratch. - Correlated values already work — Faker<T>.RuleFor((f, x) => ...), CustomInstantiator, FinishWith, and nested Faker<TNested> generation are all ordinary Faker<T> API, entirely inside the configureFaker callback; this package adds nothing on top of them. This satisfies docs/mvp.md's "Initial correlated-value experiment" scope item without a new Compono mechanism — see the .DependsOn(...) deferral below.
.DependsOn(...) correlated-rule API — deferred¶
docs/mvp.md's "Initial correlated-value experiment" is satisfied by Model 3's Faker<T> (above), not by a new Compono-native member-dependency mechanism. .For<T>().Member(...).DependsOn(...).UseBogus(...) is not built in this milestone: it would require new core semantics (member composition ordering, retaining already-composed sibling values for a later rule to read, dependency- cycle detection, interaction with constructor-parameter/required-member ordering) that are a materially larger scope than "integrate Bogus," and would duplicate capability Faker<T> already provides natively. docs/public-api.md's existing .DependsOn(...) sketch is retained as an explicitly deferred future possibility, not deleted — see Links.
Coexistence with Compono.NSubstitute¶
No shared code, no reference between the two packages in either direction — cooperation falls out entirely of the existing fixed pipeline order (docs/architecture.md) plus each provider's own narrow type claim:
- Stage order is fixed: semantic providers (stage 5, Bogus) are always tried before test-double providers (stage 6, NSubstitute), for every request, regardless of registration order between
UseBogus()/UseNSubstitute()in a profile or builder chain. - Disjoint type claims, by construction, not by coordination.
BogusMemberNameProvideronly ever claimsRequestedType == typeof(string);NSubstituteProvideronly ever claims an interface, delegate, or (optionally) unsealed abstract class —stringis none of those. Neither provider needs to know the other package exists for this to hold; it's a consequence of what each provider's static type check actually is. - Explicit registrations and configuration rules still win over both — stage ¾ run before stage ⅚ unconditionally, so a
Register<T>/.For<T>().Use(...)for a member both packages might otherwise touch always takes precedence, with no special-casing needed in either package. [Shared]reuse composes across both: a[Shared]interface parameter substituted byNSubstituteProviderand an ordinary string member supplied byBogusMemberNameProviderboth write into the sameCompositionScope, exactly like any other stage's successful result (ADR-0021) — no interaction to design, since scope reuse has never been provider-specific.UseBogus()andUseNSubstitute()in the same profile, any call order — since neither reads the other's configuration and stage placement is fixed independent of registration order,builder.UseNSubstitute().UseBogus()andbuilder.UseBogus().UseNSubstitute()compose identically.
Package boundary¶
Compono.Bogus depends on Compono and Bogus only, matching the existing package-dependency diagram (docs/architecture.md). BogusMemberNameProvider/ BogusOptions/CompositionBuilderExtensions/MemberRuleExtensions are the package's entire public surface. Compono.Bogus never references Compono.NSubstitute, and vice versa.
Positive Consequences¶
- All three customization models compile to existing pipeline mechanism (stage 5 provider, stage 4 rule, stage 3 registration) — zero new core mechanism beyond ADR-0026's
DeriveSeed(). - Coexistence with
Compono.NSubstituterequires no coordination code in either package — provably disjoint by each provider's own static type check, verified by test rather than by mutual awareness. - Correlated values are satisfied by Bogus's own
Faker<T>, not a new, independently-maintained Compono mechanism that would duplicate it. - Locale is never implicitly coupled between activation and whole-object generation — no hidden call-order dependency to document or get wrong.
Negative Consequences¶
.DependsOn(...)remains undesigned — a real gap for a consumer who wants member-level (not whole-object) correlated rules without adoptingFaker<T>for that entire type. Accepted as a deliberate scope cut, revisit only if Milestone 7 dogfooding surfaces a real needFaker<T>doesn't already cover.- Two separate locale parameters (
BogusOptions.Localefor Model 1, a per-calllocaleparameter for Models ⅔) means a consumer wanting one consistent locale across all three models repeats the string — a small, deliberate ergonomics cost in exchange for no hidden coupling (see Decision Drivers).
Pros and Cons of the Options¶
Correlated values via Faker<T> (chosen)¶
- Good, because it needs zero new Compono mechanism.
- Good, because it's exactly what Bogus's own API is already designed for.
- Bad, because it only covers the whole-object-generation case, not a correlated member-level rule layered onto an otherwise-generated-plan type.
Correlated values via a new .DependsOn(...) mechanism¶
- Good, because it would let a consumer correlate one member against a sibling without adopting whole-object
Faker<T>generation for the entire type. - Bad, because it requires new core ordering/retention/cycle-detection semantics well beyond "integrate Bogus" — a separate design problem in its own right.
- Bad, because it duplicates capability
Faker<T>already provides natively.
UseBogus<T>() as sugar over Register<T> (chosen)¶
- Good, because duplicate-registration conflict behavior, build-time validation, and stage-3 precedence all come for free, already tested by
Compono.Tests' existingRegister<T>coverage. - Good, because it's a first-class, discoverable Bogus-specific entry point despite adding zero new pipeline concept.
- Good, because constructing a fresh
Faker<T>inside the factory (rather than once, outside it, and reused) means the concurrency questionRegister<T>'s "factory closure, invoked repeatedly" shape might otherwise raise never arises — no instance is ever shared across requests to begin with.
UseBogus<T>() fully independent of package-wide locale (chosen)¶
- Good, because behavior never depends on builder call order.
- Good, because "own locale, no dependency" is the same posture core's own scalar-configuration rules already use (set once, no implicit cross-reads).
- Bad, because a consumer wanting one shared locale across every Bogus feature states it twice rather than once.
Amendment 1 (2026-08-01): UseSeed() must run before configureFaker, not after¶
PR #33 review (Codex, one P2 finding against Phase 1's implementation of this ADR's Model 3) caught a real defect in this ADR's own UseBogus<T>() code sketch: configureFaker(faker) ran before faker.UseSeed(context.DeriveSeed()). Faker<T>.RuleFor factories (f => f.Internet.Email()) are lazy — they don't draw randomness until Generate() runs, so those are unaffected — but a configureFaker callback that eagerly reads randomness at configuration time (e.g. RuleFor(x => x.Id, faker.Random.Guid()), an already-evaluated value rather than a lazy factory delegate) draws from faker.Random before this ADR's own seed had been applied, using Bogus's own default, unseeded Randomizer state instead. Two Create<T>() calls with the same Compono seed could then produce different objects for that one eagerly-read member — exactly the determinism contract this whole design exists to guarantee, broken by the sketch's own statement order, not by anything a consumer did wrong.
Fix: apply UseSeed(context.DeriveSeed()) immediately after constructing Faker<T>, before calling configureFaker, then call Generate() last:
var faker = new Faker<T>(locale).UseSeed(context.DeriveSeed());
configureFaker(faker);
return faker.Generate();
UseSeed(...) sets the instance's Random immediately (not lazily) and that state persists for both configureFaker's own execution and the later Generate() call, so one seed application covers both an eager read inside configureFaker and every lazy RuleFor factory Generate() evaluates. This is a statement-order correction only — every architectural guarantee this ADR's Model 3 section commits to (fresh Faker<T> per request, never shared/ cached, no new pipeline mechanism) is unchanged. BogusMemberNameProvider and the member-rule UseBogus(...) sugar (Models ½) were already correct — both construct their Faker/apply its Random before calling into user code, via an object initializer (new Faker(locale) { Random = new Randomizer(...) }) rather than a separate statement, so this defect was scoped to Model 3 only.
Caught and fixed within PLAN-0006 Phase 1, before any external consumer or its test-suite phase existed — docs/plans/0006-milestone-6-bogus-integration.md's own Phase 3 (Test suites and verification — renumbered from Phase 2 after ADR-0028's later Phase 2 insertion) task list now includes explicit regression coverage for this ordering (an eager-random-read configureFaker callback, asserted deterministic for the same seed).
Links¶
- docs/mvp.md — Milestone 6 scope, non-goals, exit criterion
- docs/public-api.md — Bogus Integration section (this ADR replaces its
context.Semantic.Email()sketch and its.DependsOn(...)sketch's status, from "design goal" to "explicitly deferred, see this ADR") - docs/architecture.md —
Compono.BogusPackage Boundaries entry, stage 5 (semantic value providers) - ADR-0011 — the shared-scope mechanism the coexistence section relies on unchanged
- ADR-0012 — the path-independence guarantee this package's values inherit via
DeriveSeed() - ADR-0019 — the
Register<T>mechanismUseBogus<T>()compiles into, including its duplicate-registration conflict behavior - ADR-0020 — the
.For<T>().Member(...).Use(...)mechanism the member-levelUseBogus(faker => ...)sugar wraps - ADR-0024 — the public provider contract
BogusMemberNameProviderimplements; the Bogus sketch this ADR supersedes with a real design - ADR-0025 —
Compono.NSubstitute, the package this ADR's Coexistence section verifies against with no code shared in either direction - ADR-0026 — the
DeriveSeed()capability every model in this ADR builds its determinism on