Compliance wave 2: EventProjection, auto-discovery and rebuild-cap suites (2.37.2) - #608
Merged
Merged
Conversation
…ites (2.37.2) Companion to marten#5118 (epic marten#5110, program marten#5119). Four more suites lifted out of the hand-mirrored Marten/Polecat pairs, 13 tests, taking the shared library from 42 to 55. - AutoDiscoveredAggregateCompliance: a self-aggregating type with a generated evolver has to work with nothing registered at all. Distinct from SelfAggregatingEvolveCompliance, where the same aggregates are registered as inline snapshots and discovery is never exercised. - EventProjectionRegistrationCompliance: document types are discovered from a Store<T> call inside an explicit ApplyAsync override (marten#4166) and from a conventional Create method's return type, plus an end-to-end write proving the store really provisioned storage for a type nobody registered. Strongest assertion wins: Marten's copy only checked the store's known document types, Polecat's only checked PublishedTypes(); the shared suite does both and adds the round trip. - EventProjectionEnrichmentCompliance: EnrichEventsAsync runs before an inline EventProjection applies the same batch and can read documents from the store while it does. Each projection gets its own event and document type, since all three are registered against one store. - RebuildConcurrencyCapCompliance: resolution of IEventStore.MaxConcurrentRebuildsPerDatabase (#420 / marten#4710) -- explicit setting wins, non-positive disables, otherwise pool size over eight with a floor of one, and the usage descriptor carries the effective value. Seam additions, all driven by a suite that needed them: StoreDocument (seeding a lookup document the event store did not produce), EventStore (store-level contracts), AllAggregateTypes (the graph is shared, reaching it is not), and ComplianceStoreConfig.AddProjection / MaxConcurrentRebuildsPerDatabase / MaxPoolSize. The two cap knobs bypass IComplianceStoreRegistrar on purpose: the products hang them off different objects (Marten Projections, Polecat DaemonSettings) and MaxPoolSize has to reach the connection string, which only the fixture owns. Consumers now supply three global aliases rather than one. The EventProjection suites declare projection types at file scope, so they cannot reach the <TOperations, TQuerySession> pair the suite classes are generic over -- ComplianceOperations and ComplianceEventProjection close that the same way ComplianceQuerySession already did for the self-aggregating fixtures. Aliases rather than a shared generic base because both products' EventProjection carries store-specific members. Note for anyone writing to this suite later: the generator's scan of ApplyAsync bodies for published types is syntactic, so Store<T> must be written with an explicit type argument. Store(entity) compiles and silently registers nothing. Verified against both stores through the packed package: Marten 55/55, Polecat 55/55, still zero capability gates on either. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VpDCvJcBDZerieJB4JEHde
This was referenced Aug 2, 2026
Open
jeremydmiller
added a commit
that referenced
this pull request
Aug 2, 2026
…e_shard (#609) ShardStateTracker publishes through a Block<ShardState>, so a subscribed observer runs on the block's consumer thread and is not guaranteed to have run by the time StartAgentAsync returns. The test collected Paused states into a List and asserted on it immediately, which is a race it lost on net9 in CI while a compliance-only PR was in flight (#608). The failure message is what identifies it rather than a wrong count: Shouldly.ShouldAssertException : [ShardName: Trip:V2:All, Sequence: 60, Action: Paused] should have single item but had 1 items A one-element collection reported as failing a single-item assertion is not possible unless the collection changed between the check and the message being rendered. The list was empty when ShouldHaveSingleItem ran and held the state a moment later when Shouldly enumerated it again to build the message. Waits for the publication through a TaskCompletionSource bounded by the class's TestTimeout, which was declared for exactly this and never used. Deliberate narrowing: the exactly-one-publication assertion is gone, because "exactly one so far" cannot be asserted against an asynchronous publisher without an arbitrary sleep, and the test's subject is that a supervisor sees the pause with the right sequence and reason -- not the publication count. net9.0: the class 8/8 consecutive runs, EventTests 657/0/0. Claude-Session: https://claude.ai/code/session_01VpDCvJcBDZerieJB4JEHde Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
jeremydmiller
added a commit
that referenced
this pull request
Aug 2, 2026
…lly (#611) * Discover EventProjection published types semantically, not syntactically Closes the trap that cost compliance wave 2 (#608) a red test and is far worse for users than for suite authors. AggregateAnalyzer.DiscoverDocumentTypesFromMethodBodies matched only GenericNameSyntax, so an explicit ApplyAsync override that wrote operations.Store<AuditRecord>(record); registered AuditRecord as a published type, while the equally valid operations.Store(record); compiled and registered nothing at all. Nothing at the call site says so, and nothing fails at runtime either: the store provisions that document's storage on demand, so only the ahead-of-time surfaces come up short -- schema creation, known document types, rebuild teardown. See marten#4166 for the original. Binding the invocation gives the same answer for both spellings, because the type argument is on the method symbol whether or not it was written down. The syntactic path stays as a fallback for trees that do not bind (mid-edit, broken code), so nothing regresses when the semantic model has nothing to say. Also fixes a hole the semantic path made visible: IsFrameworkType cannot decide whether a type is registrable, because object and string render through ToDisplayString() as their C# keywords rather than System.Object / System.String, so a name-prefix test let them through. Store<object>(...) was registering `object` as a published document type. Registrability is now a SpecialType / TypeKind question, and IsFrameworkType is only the last check. New JFXEVT005 (Info) covers what is left: a call that binds to the projection's own session but whose document type cannot be named -- object, dynamic, an open type parameter. Info rather than Warning on purpose. Registration is not always required (the type may be registered through store options, and storage is provisioned on demand regardless), so this must not break a TreatWarningsAsErrors build over a legitimate call. Receiver identification is deliberate rather than name-based: the call must bind to the projection's own TOperations (directly, through an interface, or as an extension method's first parameter), so an unrelated Store method on some other object produces neither a registration nor a diagnostic. JasperFx.Events.SourceGenerator.Tests 30/30 with four new cases covering both spellings, the unregistrable case, and the unrelated-receiver case. EventTests 657/0/0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VpDCvJcBDZerieJB4JEHde * 2.37.3 The published-type discovery change ships in JasperFx.Events.SourceGenerator, so it needs its own version. The publish workflow pushes with --skip-duplicate: run at an already-published version it skips every package and still reports success, so a shippable change merged without a bump goes out as nothing at all. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Companion to marten#5118 (epic marten#5110, program marten#5119), and the follow-on to #607. Four more suites lifted out of the hand-mirrored Marten/Polecat pairs — 13 tests, taking the shared library from 42 to 55.
Suites added
AutoDiscoveredAggregateComplianceSelfAggregatingEvolveCompliance, where the same aggregates are registered as inline snapshots and discovery is never exercised.EventProjectionRegistrationComplianceStore<T>call inside an explicitApplyAsyncoverride (marten#4166) and from a conventionalCreatereturn type, plus an end-to-end write proving the store really provisioned storage for a type nobody registered.EventProjectionEnrichmentComplianceEnrichEventsAsyncruns before an inline EventProjection applies the same batch, and can read documents from the store while it does.RebuildConcurrencyCapComplianceIEventStore.MaxConcurrentRebuildsPerDatabase(#420 / marten#4710): explicit setting wins, non-positive disables, otherwise pool size over eight with a floor of one, and the usage descriptor carries the effective value.Strongest-assertion-wins in action: Marten's registration test only checked the store's known document types, Polecat's only checked
PublishedTypes(). The shared suite does both and adds the round trip.Seam additions
Each one exists because a suite needed it, not on speculation:
EventStoreComplianceFixture.StoreDocument— seeding a lookup document the event store did not produce.EventStoreComplianceFixture.EventStore— store-level contracts (MaxConcurrentRebuildsPerDatabase,TryCreateUsage).EventStoreComplianceFixture.AllAggregateTypes—ProjectionGraph.AllAggregateTypes()is shared, but the graph hangs off each product's own options type.ComplianceStoreConfig.AddProjection+IComplianceStoreRegistrar.AddProjection.ComplianceStoreConfig.MaxConcurrentRebuildsPerDatabase/MaxPoolSize. These bypass the registrar deliberately: the products hang the cap off different objects (MartenProjections, PolecatDaemonSettings), andMaxPoolSizehas to reach the connection string, which only the fixture owns.Consumer-visible change
Consumers now supply three global aliases instead of one. The EventProjection suites declare projection types at file scope, so they cannot reach the
<TOperations, TQuerySession>pair the suite classes are generic over:Aliases rather than a shared generic base, because both products'
EventProjectioncarries store-specific members (Marten'sIProjectionSchemaSource/IMartenRegistrable, Polecat's sealedstoreEntity). README updated.Landmine worth knowing
The generator's scan of
ApplyAsyncbodies for published types (AggregateAnalyzer.DiscoverDocumentTypesFromMethodBodies) is syntactic, soStore<T>must be written with an explicit type argument.Store(entity)compiles and silently registers nothing — that is a real trap for users, and it cost this PR one red test before the comment went in.Verification
Both stores against the packed package: Marten 55/55, Polecat 55/55, still zero capability gates on either.
Consumer PRs: marten#5123, polecat#TBD — both blocked on the 2.37.2 publish.
🤖 Generated with Claude Code
https://claude.ai/code/session_01VpDCvJcBDZerieJB4JEHde