Skip to content

GH-4181 + GH-4182: stop claiming the trigger label, and read a collection response as its element type - #4185

Merged
jeremydmiller merged 1 commit into
mainfrom
GH-4181-4182-event-model-trigger-and-read-model
Aug 29, 2026
Merged

GH-4181 + GH-4182: stop claiming the trigger label, and read a collection response as its element type#4185
jeremydmiller merged 1 commit into
mainfrom
GH-4181-4182-event-model-trigger-and-read-model

Conversation

@jeremydmiller

@jeremydmiller jeremydmiller commented Aug 29, 2026

Copy link
Copy Markdown
Member

Closes #4181, closes #4182.

Two Event Model derivation defects found while wiring the four-source test vehicle (JasperFx/bobcat#172, part of the SDD effort). Both land in EventModelRoles.Describe, which is why they ship together.

GH-4181 — HTTP and gRPC slices claimed TriggerLabel

Describe passed seed.TriggerOrigin?.Label into the TriggerLabel slot, and HttpEventModelSource.ForChain always populates that origin with "{verb} {route}". So every HTTP slice claimed the role on the Derived rung, per-role precedence (#703) made it beat the overlay's Declared claim, and the merge recorded a SourceDisagreement hotspot for the dropped claim. Five labelled endpoints meant five noise hotspots drowning out any real finding.

The claim carried no information. The route and verb already ride on TriggerOrigin, structured and lossless, and a viewer resolves the trigger element from TriggerType / TriggerLabel / TriggerOrigin together — asserted in the new test, so withholding the label costs the canvas nothing. A trigger label is a naming concern ("Customer at the ATM"), exactly what the SDD decisions reserve for declarations: the code cannot express it. With the role unclaimed, an overlay's label wins by default because nothing else claims it — which is the contract #703's own remarks state.

grpcTriggerOnlySlice had the same duplication (it sets the origin two lines below the claim) and gets the same treatment.

Deliberately not changed: ApplyExternalSystems still claims TriggerLabel. When a listener triggers a slice that already has an origin, the slice keeps its own origin and the external label lives nowhere else — so unlike the two above, that claim is not a duplicate of something already carried, and nulling it would lose the label outright.

GH-4182 — a collection response reported the raw generic CLR string

A View slice took seed.ResponseType verbatim, so

[WolverineGet("/api/orders/list")]
public static Task<IReadOnlyList<Order>> Get(...)

reported System.Collections.Generic.IReadOnlyList1[[WolverineWebApi.Marten.Order, WolverineWebApi, Version=…as its read model — an unreadable canvas node sitting right next to theOrder` node its single-document siblings name.

A collection response now reads its element type, folding the slice onto the node that already exists. The unwrap walks interfaces rather than matching a whitelist of collection types, so Marten's IPagedList<Order> (a page of Order is still a view over Order) unwraps too. Left alone: maps (a Dictionary<K,V> enumerates as KeyValuePair<,>, which is not a read model anybody drew), string, a type that enumerates as two different things, and scalar elements — a byte[] download is not a view over byte, and folding every list-of-string route onto one String node would be a worse canvas than the one this fixes.

Tests

  • event_model_trigger_label_4181 (new) — the derived slice leaves the role unclaimed while the origin still names the route; the wireframe trigger element still renders POST /orders/ship3; and a Declared overlay label merged first wins the role with no SourceDisagreement hotspot, without taking any derived role with it.
  • event_model_read_model_collection_4182 (new) — IReadOnlyList<Order> and IPagedList<Order> both read Order, asserted on FullName against the single-document route so a match on Name alone cannot pass it vacuously.
  • event_model_roles_3988 (HTTP) and event_model_per_rpc_4000 (CoreTests) now assert the route/RPC off TriggerOrigin.Label — the fact moved, it did not disappear.
  • One new endpoint in WolverineWebApi (GET /api/orders/list) for the literal shape Event model: a query endpoint returning a collection reports the raw IReadOnlyList`1 CLR string as its read model, not the element type #4182 was reported against.

Verification

  • dotnet build wolverine.slnx -c Release -f net9.0 — clean.
  • Wolverine.Http.Tests 1003 passed, 0 failed; CoreTests 2671 passed, 0 failed; Wolverine.Grpc.Tests 306 passed, 0 failed; MartenTests EventModeling 7/7.

Follow-ups outside this PR

Both filed against the bobcat vehicle, and both gated on samples/BankAccountES/BankAccountES.csproj bumping its WolverineFx.* pin off 6.30.1 to whatever release carries this:

🤖 Generated with Claude Code

…tion response as its element type

Two Event Model derivation defects found by the four-source test vehicle
(JasperFx/bobcat#172), both in EventModelRoles.Describe.

GH-4181. An HTTP slice claimed TriggerLabel with "{verb} {route}", and a gRPC
trigger-only slice claimed it with "{service}/{method}". Both are Derived
claims, so per-role precedence (#703) made them beat any label an
overlay declared -- and minted a SourceDisagreement hotspot per labelled route
for the privilege, so five labelled endpoints meant five noise hotspots
drowning out any real finding. The claim carried no information: both facts
already ride, structured, on TriggerOrigin, and the wireframe's trigger element
resolves from TriggerType / TriggerLabel / TriggerOrigin together. A trigger
label is a naming role -- "Customer at the ATM", which the code cannot express
-- so the role is now left unclaimed and a declaration wins it by default.

ApplyExternalSystems still claims TriggerLabel, deliberately: when a listener
triggers a slice that already has an origin, the external label lives nowhere
else, so it is not the same duplication.

GH-4182. A query chain's response was taken as its read model verbatim, so
GET => Task<IReadOnlyList<Order>> reported the closed generic's
assembly-qualified CLR string as a canvas node sitting next to the "Order" node
its single-document siblings name. A collection response now reads its element
type, folding the slice onto the node that already exists. The unwrap walks
interfaces rather than matching a whitelist so Marten's IPagedList<Order>
unwraps too; maps, strings, types that enumerate as two things, and scalar
elements (a byte[] download is not a view over byte) are left alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jeremydmiller
jeremydmiller merged commit 6239760 into main Aug 29, 2026
39 checks passed
@jeremydmiller jeremydmiller mentioned this pull request Aug 30, 2026
saithis pushed a commit to saithis/wolverine that referenced this pull request Sep 1, 2026
…1.0-alpha.1, Polecat 4.6.0-alpha.1

Bumps JasperFx 2.13.3 → 2.14.2 (document-diagnostics surface + the three projection
codegen fixes: JasperFx#4185 DI-activated projection evolver, JasperFx#297 multi-identity dispatcher,
derived/custom-named projection evolver inheritance), Marten 9.8.0 → 9.11.0-alpha.1
(+ Marten.AspNetCore / Marten.Newtonsoft), Polecat 4.5.0 → 4.6.0-alpha.1.

Co-Authored-By: Claude Opus 4.8 (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

1 participant