Skip to content

GH-992..995: event model definitions — app model name, discovery manifest, ForAggregate, spec links - #996

Merged
jeremydmiller merged 1 commit into
mainfrom
GH-992-995-event-model-definitions
Oct 9, 2026
Merged

jeremydmiller merged 1 commit into
mainfrom
GH-992-995-event-model-definitions

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Closes #992, closes #993, closes #994, closes #995.

#992 Definitions join the application's model by default

  • EventModelDefinition.Name is now string? and defaults to null, meaning the application's model. Overrides that return a string still compile.
  • EventModelDiscovery.ApplicationModelName(services) is public, so other sources can reuse the rule. It resolves to:
    • JasperFxOptions.ServiceName, which Wolverine keeps in step with WolverineOptions.ServiceName, and which Marten already uses;
    • otherwise the entry assembly's name;
    • otherwise "EventModel".
  • Each slice's Origin still names its own definition class (event-model://{ClassName}). For an instance with no name, Subject uses the class name too.
  • Existing discovery tests that expected the class name as the model name have been updated.

#993 AOT-safe discovery of definitions

  • New EventModelDiscoveryGenerator in JasperFx.SourceGenerator.
    • It lists every public, concrete, non-generic EventModelDefinition subclass that has a public constructor; definitions nested inside public types are included.
    • The list is a JasperFx.Generated.DiscoveredEventModels.DefinitionTypes manifest. [DynamicDependency(PublicConstructors)] keeps each listed constructor through trimming.
    • An empty manifest is still emitted whenever the compilation can see EventModelDefinition, so "generator ran, found nothing" is distinguishable from "generator never ran".
  • New AddDiscoveredEventModels(assembly): reads the manifest and enumerates no types. Without a manifest it falls back to the reflective scan (the trim warning is suppressed, with a justification, as CommandFactory does).
  • AddEventModelsFromAssembly also prefers the manifest now.

#994 ForAggregate / NoAggregate / DeciderModel

  • ForAggregate: ForAggregate<T>() / ForAggregate(string) set a default aggregate.
    • The last call wins, and it applies only to Command slices opened after it that declare no aggregate of their own.
    • It never carries into another definition.
    • It also declares the aggregate on the model. Aggregate<T>() is unchanged.
  • NoAggregate() and DeciderModel<T>() on a slice override the default. Calling NoAggregate() together with Against / StartsStream throws.
  • The slice records why it has its aggregate:
    • EventModelSliceDescriptor.AggregateDeclaration { Default, Explicit, None, DeciderModel } and DeciderModel.
    • Two new roles, appended to EventModelRole, so they take part in the merge.
    • A default-applied aggregate is still a Declared claim: the code wins, and any difference becomes a hotspot.

#995 Specification links without strings

  • Typed links: LinksToSpecification<TSpec>(nameof(TSpec.scenario)).
    • It uses SpecificationIdentity, a twin of Bobcat's current MarkerSpecNaming rule.
    • [BobcatFeature] is matched by attribute name, through CustomAttributeData.
    • The tests reuse Bobcat's own examples, e.g. apply_to_volunteer → apply to volunteer/volunteer application submitted.
    • Bobcat's code-first lane is gone. This matches the projected (xUnit/TUnit) rule that Bobcat uses today.
  • The contract: SpecificationBindingDescriptor carries the spec identity, CommandType, and optionally Domain, Namespace, an explicit SliceName, and ResolvedTypes.
  • The join: EventModelSpecifications.Link(model, manifest) is a pure function over an assembled model.
    • Specs join slices by command type. When several slices handle the same command, the spec's domain or namespace picks one.
    • An explicit slice name overrides the inference.
    • A join that is still ambiguous, or an explicit slice that doesn't exist, raises a model-level HotspotOrigin.UnresolvedSpecification hotspot instead of guessing.
  • New rung EventModelProvenance.Specified:
    • Its wire value is 3, but it ranks between Declared and Derived.
    • Merge now compares rungs through Rank() / Outranks(), not the raw enum value.
    • A declared link the specs don't include loses and becomes a SourceDisagreement hotspot.
  • Where the join runs (the issue's open question) is not settled in this PR. Only the contract and the join are here; the monitor or a --specs option are follow-ups.

Wire / downstream

  • New wire members (all appended): Specified, UnresolvedSpecification, AggregateDeclaration, and the EventModelRole additions.
  • event-model-vue types and enum pins are updated; type-check is clean and all 225 tests pass.
  • CritterWatch needs to learn Specified and UnresolvedSpecification.
  • JasperFx.SourceGenerator.Tests now references JasperFx.Events.

Tests

  • EventTests: 1433 passing on both net9.0 and net10.0. That includes new ApplicationModelNameTests, DiscoveredEventModelsTests (a stand-in Assembly whose type enumeration throws), ForAggregateTests and SpecificationLinkTests, plus a wire round-trip extended to the new members.
  • CoreTests: 656 passing on both net9.0 and net10.0.
  • JasperFx.SourceGenerator.Tests: 29 passing, including 10 new manifest tests.
  • The solution builds with 0 errors and no new trim warnings.

🤖 Generated with Claude Code

https://claude.ai/code/session_01J8KxC18Q51z5w4u6YARwe4

…efault aggregates and spec links

- GH-992: EventModelDefinition.Name defaults to null, meaning the application's model
  (EventModelDiscovery.ApplicationModelName: JasperFxOptions.ServiceName, else the entry
  assembly). Per-chapter definitions now merge with the code-derived model; Origin still
  names each definition class.
- GH-993: JasperFx.SourceGenerator's EventModelDiscoveryGenerator emits a per-assembly
  JasperFx.Generated.DiscoveredEventModels manifest (constructors rooted with
  DynamicDependency). New AddDiscoveredEventModels(assembly) reads it with no type scan;
  AddEventModelsFromAssembly prefers it too, falling back to the reflective scan.
- GH-994: EventModelBuilder.ForAggregate<T>() / ForAggregate(string) default for following
  command slices; NoAggregate() and DeciderModel<T>() on a slice. Slices carry
  AggregateDeclaration { Default, Explicit, None, DeciderModel } and DeciderModel.
- GH-995: typed LinksToSpecification<TSpec>(scenario) via SpecificationIdentity (Bobcat's
  naming rule); SpecificationBindingDescriptor contract and EventModelSpecifications.Link
  join by command type (+ domain / namespace, or explicit slice); new
  EventModelProvenance.Specified rung (wire 3, ranked between Declared and Derived via
  Rank/Outranks) and HotspotOrigin.UnresolvedSpecification for ambiguous joins.
- Viewer types and docs updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KxC18Q51z5w4u6YARwe4
@jeremydmiller
jeremydmiller merged commit fa06b5f into main Oct 9, 2026
4 checks passed
@jeremydmiller
jeremydmiller deleted the GH-992-995-event-model-definitions branch October 9, 2026 17:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment