Skip to content

Compliance wave 2: EventProjection, auto-discovery and rebuild-cap suites (2.37.2) - #608

Merged
jeremydmiller merged 1 commit into
mainfrom
feat/compliance-wave-2
Aug 2, 2026
Merged

Compliance wave 2: EventProjection, auto-discovery and rebuild-cap suites (2.37.2)#608
jeremydmiller merged 1 commit into
mainfrom
feat/compliance-wave-2

Conversation

@jeremydmiller

@jeremydmiller jeremydmiller commented Aug 2, 2026

Copy link
Copy Markdown
Member

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

Suite What it pins down
AutoDiscoveredAggregateCompliance A self-aggregating type with a generated evolver works with nothing registered. Distinct from SelfAggregatingEvolveCompliance, where the same aggregates are registered as inline snapshots and discovery is never exercised.
EventProjectionRegistrationCompliance Document types discovered from a Store<T> call inside an explicit ApplyAsync override (marten#4166) and from a conventional Create return type, plus an end-to-end write proving the store really provisioned storage for a type nobody registered.
EventProjectionEnrichmentCompliance EnrichEventsAsync runs before an inline EventProjection applies the same batch, and can read documents from the store while it does.
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.

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.AllAggregateTypesProjectionGraph.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 (Marten Projections, Polecat DaemonSettings), and MaxPoolSize has 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:

global using ComplianceQuerySession = Marten.IQuerySession;
global using ComplianceOperations = Marten.IDocumentOperations;      // new
global using ComplianceEventProjection = Marten.Events.Projections.EventProjection;  // new

Aliases rather than a shared generic base, because both products' EventProjection carries store-specific members (Marten's IProjectionSchemaSource/IMartenRegistrable, Polecat's sealed storeEntity). README updated.

Landmine worth knowing

The generator's scan of ApplyAsync bodies for published types (AggregateAnalyzer.DiscoverDocumentTypesFromMethodBodies) is syntactic, so Store<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

…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
@jeremydmiller
jeremydmiller merged commit 9d7dac3 into main Aug 2, 2026
1 of 2 checks passed
@jeremydmiller
jeremydmiller deleted the feat/compliance-wave-2 branch August 2, 2026 16:49
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant