catch up with dev - #34
Merged
Merged
Conversation
… emlang enrichments Brings ShelterAdoption's two already-built chapters in line with Spec/K9CRUSH.emlang.v3.yaml, ahead of building v3's new SurrenderingYourDog/ FosteringADog/VolunteeringAndHomeChecks chapters (which depend on this foundation - FosteringADog's cascades reference DogListing.Status directly). TheWouldBeAdopter: SubmitApplicationRequest now carries an 18-field household/lifestyle intake questionnaire (home ownership/type, garden, children, other pets, daily routine, energy preference, prior experience, data processing consent), captured on Application.Intake at the point of submission via a new ApplicationIntake value object. Conditionally-required fields (GardenSize/GardenEnclosed/ChildrenAgeRange/OtherPetsDetails) validated via IValidatableObject. ShelterManagingListings: DogListing gains a Status field (Available|NotReadyYet|InFoster|PendingAdoption|Adopted, Available=0 so pre-existing documents deserialize as adoptable) plus a new UpdateListingStatusHandler for shelter staff to set it manually. New listings now start NotReadyYet instead of implicitly-available. Two consequences of that: GetAdoptionListings now filters to Status == Available so newly-added/adopted dogs stop appearing in public browse, and ApproveApplicationHandler now cascades the listing's status to Adopted (a same-module state change, not a cross-module event) - both explicitly called out in the v3 spec's ShelterManagingListings comment. Also fills two pre-existing test gaps surfaced while touching this code: DogListing had no Layer 1 domain tests at all, and SubmitApplicationHandler had no direct test coverage beyond the drafts integration test (query-based, Layer 3 only). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
An event-modeling checklist pass (validated against https://github.com/Nebulit-GmbH/Eventmodelers-Build-Kits's eventmodeling-validating-event-models-checklist skill) flagged that UpdateListingStatusHandler had no transition guard at all - a shelter could mark a listing Adopted with no approved Application on file, or flip an already-adopted listing back to Available/InFoster/etc. by hand. Adopted is now a one-way door: not a settable target via this endpoint (only ApproveApplicationHandler's cascade can reach it) and not editable once reached. Spec/K9CRUSH.emlang.v3.yaml's ShelterManagingListings chapter updated to match - the command's props no longer list Adopted as a legal input, and a new blocked-outcome pseudo-event ("Listing Status Update Blocked: Adopted Requires Approval") documents the guard in both directions, same convention as this file's other blocked-outcome events. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Builds Spec/K9CRUSH.emlang.v3.yaml's SurrenderingYourDog chapter - a
member surrendering their own dog into a shelter's care, distinct from
TheWouldBeAdopter's applicant journey. Built directly from the yaml
(eventmodelers board is currently unreachable - "organization license is
not active" - user opted to proceed without the load-slice pipeline for
this slice rather than pause).
New DogSurrenderRequest entity (Requested -> UnderReview ->
{AdditionalDetailsRequested -> UnderReview | Accepted | Declined}) plus
seven slices:
- RequestDogSurrender (any verified owner)
- GetSurrenderReviewQueue (Admin, platform-wide, unfiltered by status)
- ReviewSurrenderRequest, RequestAdditionalSurrenderDetails,
AcceptDogSurrender, DeclineDogSurrender (Admin, no ownership check -
same platform-wide reasoning as ApproveShelterAccountHandler)
- SubmitAdditionalSurrenderDetails (ownership-gated to the surrendering
member, no request body - same shape as Application's identically-named
step)
AcceptDogSurrender cascades into ShelterManagingListings' Add Dog Listing
(status NotReadyYet) in the same session - TemperamentNotes becomes the
new listing's Bio, HealthNotes stays on the surrender record only.
ShelterAccountId was a necessary disclosed gap-fill on that request: the
yaml never specifies which shelter receives the dog, but DogListing.Create
requires one.
25 new tests (104 -> 129 in ShelterAdoption.Tests), all Layer 1-2 except
GetSurrenderReviewQueue's unscoped query (Layer 3, own dedicated container
per the established shared-collection-pollution gotcha). v3 yaml updated:
chapter tag flipped [PLANNED] -> [BUILT], gap-fill and no-request-body
decisions documented inline.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Builds Spec/K9CRUSH.emlang.v3.yaml's FosteringADog chapter - a member providing temporary foster care for a shelter's dog. Built directly from the yaml (eventmodelers board still returns a 402 license error). New FosterApplication entity (Submitted -> UnderReview -> Approved | Rejected, reuses Application's HomeType enum) plus 7 slices covering becoming an approved caregiver and placing/ending a foster placement: ApplyToFoster, GetFosterApplicationsQueue, ReviewFosterApplication, ApproveFosterCaregiver, RejectFosterApplication, PlaceDogInFoster, MarkFosterDogReadyForAdoption, EndFosterPlacement - all Admin-gated per the yaml's own swimlane, no ownership check (platform-wide, same reasoning as the Surrender chapter's Admin actions). Foster placement is a status change on the EXISTING DogListing, not a separate document, per the chapter's own framing - added DogListing.CurrentFosterCaregiverOwnerId to track who's currently fostering. It deliberately survives MarkFosterDogReadyForAdoption's Status flip back to Available (the caregiver is still fostering until EndFosterPlacement resolves it, even while the listing is open to other applicants again) and respects Adopted's one-way-door rule. UpdateListingStatusHandler now also rejects manual status changes while a placement is active, to stop that field going stale. "Convert Foster To Adoption" needed no new command at all - it IS TheWouldBeAdopter's SubmitApplicationHandler, called by the foster caregiver for their own fostered dog, no special-casing (documented via cross-reference). "Place Dog In Foster" needed one disclosed traceability fix: takes fosterApplicationId instead of the yaml's bare fosterCaregiverOwnerId, deriving the caregiver from the loaded, Approved FosterApplication rather than trusting a caller-supplied id. 30 new tests (129 -> 159 in ShelterAdoption.Tests), all Layer 1-2 except GetFosterApplicationsQueue's unscoped query (Layer 3, dedicated container). v3 yaml updated: chapter tag flipped [PLANNED] -> [BUILT], deviations documented inline. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
First slice of the v3 VolunteeringAndHomeChecks chapter, placed in ShelterAdoption alongside Surrender/Foster. A verified owner applies to volunteer with an area of interest, creating a VolunteerApplication in Submitted status. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…iew) Second slice of the chapter - Admin's read model over every VolunteerApplication, unfiltered by status, same pattern as GetFosterApplicationsQueueHandler. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Third slice of the chapter - Admin moves a Submitted volunteer application to UnderReview, same shape as ReviewFosterApplicationHandler. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fourth slice of the chapter - Admin moves an UnderReview volunteer application to Approved, same shape as ApproveFosterCaregiverHandler. Being an "approved volunteer" is just having an Approved VolunteerApplication on file, not a new OwnerRole. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Frontend design will be done in Penpot (design system: tokens + a small core component set) and mapped onto MudBlazor's theming API rather than hand-rolling every Razor component - chosen given the team's stated design-skill gap, trading some visual distinctiveness for much faster implementation. Wires up the baseline: MudBlazor package (pinned in Directory.Packages.props), AddMudServices(), MudBlazor CSS/JS + font references in App.razor, and a MudLayout shell (AppBar + MainContent) in MainLayout.razor with a default MudTheme placeholder until Penpot's actual tokens exist. Verified live - built and ran the Blazor app, confirmed the shell renders with real MudBlazor CSS classes (mud-appbar, mud-button-filled, theme CSS variables) via a throwaway test page, then removed it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ismatch Rebuilds the placeholder feed page as real MudBlazor components (MudProgressCircular loading, MudAlert error/empty states with a Try Again button instead of an unhandled 500, MudCard/MudChip per dog). Fixes two latent bugs found while wiring it up: the page's local DiscoveryFeedEntry record didn't match GetDiscoveryFeedHandler's actual response shape (missing Name/MatchType, DistanceKm vs DistanceMiles), and the query string sent radiusKm instead of the radiusMiles the endpoint actually binds - both silently dropped/filtered real data. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
MudDrawer + MudNavMenu with a toggle button in the AppBar, so the app
has a real navigation structure future pages can hang off of - up to
now there was only one reachable route ("/"). Only one nav entry
(Discover) exists yet since it's the only page built; more get added
as pages do.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Supabase owns the entire signup/login/confirmation lifecycle (ADR-005) - Api.Host only ever validates the JWT it issues, never mints one. This adds the missing frontend half: Login.razor/Register.razor call Supabase's own /auth/v1 REST API directly via a new SupabaseAuthService, and on success sign the caller into a local cookie (CookieAuthentication) carrying the Supabase JWT as an "access_token" claim for later use against Api.Host's [Authorize]-gated endpoints. Both pages are static SSR (no @rendermode) by design - HttpContext. SignInAsync only works reliably during the classic request/response cycle a static form POST goes through, not inside an interactive Server circuit, matching the same pattern ASP.NET Core's own Identity-scaffolded Login page uses. Found and fixed a real MudBlazor gap while building this: MudTextField does not participate in static-SSR EditForm binding (confirmed empirically - it silently renders with no name attribute, so submitted values never bind). Swapped to the framework-native InputText/type=password instead, styled by hand via a small .mud-static-input CSS class, rather than pulling in an unofficial third-party static-input package for two form fields. MainLayout's AppBar now shows Log In/Log Out + the caller's email via AuthorizeView; Routes.razor upgraded to AuthorizeRouteView so future [Authorize]-gated pages work without extra plumbing; Register shows a "check your email" message rather than assuming an immediate session, since Identity's ConfirmProfile flow means Supabase won't return an access_token until the confirmation email is clicked. Verified live end-to-end against a real HTTP POST (crafted antiforgery token + cookie jar via curl): form fields bind correctly (name="Input.Email"/"Input.Password" with echoed values on failure), and a Supabase-unreachable failure now shows a friendly MudAlert instead of crashing - couldn't verify an actual successful Supabase round-trip since this session has no real Supabase project credentials (CHANGE_ME placeholders only). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New AuthorizedApiClient wraps the "K9CrushApi" HttpClient with the current session's Supabase access_token (set at login) as a Bearer header, read via AuthenticationStateProvider rather than IHttpContextAccessor - the latter is unreliable inside an interactive Server circuit, the cascading AuthenticationState survives for the circuit's lifetime instead. Account.razor ([Authorize], InteractiveServer) calls GET /api/v1/identity/me/settings and shows email/display name/role/member since/deletion status. Nav link only rendered when authenticated (AuthorizeView in MainLayout's MudNavMenu). Verified live: /account correctly 302-redirects an anonymous request to /login?ReturnUrl=%2Faccount (cookie auth's LoginPath), and the nav link is absent from the anonymous home page response. Couldn't verify the populated-card path against a real Supabase-authenticated request for the same reason as Home.razor/Login.razor - no live Supabase project credentials in this session. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Listings.razor (/listings) - MudGrid/MudCard per dog (Name, Breed) from
GET /api/v1/shelter-adoption/dog-listings, linking to
ListingDetails.razor (/listings/{id:guid}) which calls GET
.../dog-listings/{id} for the full record (AgeInMonths, Bio, Status).
Both [Authorize]-gated via AuthorizedApiClient, same pattern as
Account.razor - the backend endpoints themselves require VerifiedOwner,
not anonymous, despite "adoption browsing" sounding public.
DogListingStatus is redeclared client-side matching the backend enum's
exact declaration order, since Api.Host has no JsonStringEnumConverter
registered - the wire format is a plain int, not a string.
Verified live: both routes correctly 302-redirect anonymous requests to
/login (with ReturnUrl preserved, including for the parameterized detail
route), and the new nav link is absent from the anonymous home page.
Couldn't verify the populated-listing render path for the same reason as
prior pages - no live Supabase-authenticated request possible without
real project credentials in this session.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ApplyToAdopt.razor (/listings/{id}/apply) - the 18-field household/
lifestyle intake form for TheWouldBeAdopter's SubmitApplication slice.
Local InputModel mirrors SubmitApplicationRequest field-for-field,
including its IValidatableObject conditional rules (garden size/enclosed
required when HasGarden, children age range required when HasChildren,
other pets details required when HasOtherPets, data processing consent
required) - gives real client-side validation matching the backend
exactly rather than only catching these on round-trip.
Handles all the backend's actual response cases: 404 (listing not
found), 409 Conflict with the raw string body (open-application limit),
and 200 OK branching on WasDuplicate (backend treats "already applied"
as a no-op success, not an error). Enums (HomeOwnership/HomeType/
GardenSize/EnergyLevelPreference) redeclared client-side matching the
backend's exact ordinal order, same reasoning as DogListingStatus in
ListingDetails.razor.
ListingDetails.razor gained an "Apply to Adopt" button, shown only when
Status is Available (client-side UX guard - the backend handler itself
has no such guard per its own gap, disclosed rather than assumed).
Verified live: temporarily removed [Authorize] to confirm the full form
actually renders without runtime errors (all top-level fields present,
conditional fields correctly hidden by default), then restored it -
couldn't test an authenticated submission for the same reason as prior
pages (no live Supabase project credentials this session).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ApplicationStatus.razor (/applications/{id}) calls the real
GetApplicationStatusHandler - single application by id only, there's no
"list my applications" endpoint anywhere in ShelterAdoption (confirmed by
research pass, not assumed). Handler enforces ApplicantOwnerId == caller
server-side (403 on mismatch) - the page surfaces that as "This isn't
your application" rather than a generic error.
ApplyToAdopt.razor's success state now captures the returned
ApplicationId and links straight to this page - the only way to reach an
application's status today is via the id you get back right after
submitting (or, once other pages exist, whatever surfaces it - no list
view exists yet to browse back to it later).
Verified live: anonymous access 302-redirects to /login with ReturnUrl
preserved; temporarily un-gated the page to confirm it actually renders
(network-failure path correctly shows a friendly MudAlert, no server
exceptions), then restored [Authorize].
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
VolunteerApply.razor (/volunteer/apply) posts to the ApplyToVolunteer endpoint built earlier this session (single area-of-interest select). FosterApply.razor (/foster/apply) posts to ApplyToFoster (home type, has garden, has other pets, available-from date) - same shape as the adoption application form but smaller, no conditional fields. Both enums (VolunteerAreaOfInterest, HomeType) redeclared client-side matching the backend's exact ordinal declaration order, same reasoning as every other enum-carrying page this session. Neither module exposes a member-facing application-status lookup (Volunteer/Foster review is Admin-only), so both pages show a plain success message rather than linking anywhere - unlike ApplyToAdopt, which does have a real status endpoint to link to. Nav links added for both. Verified live: both routes correctly 302-redirect anonymous requests to /login; temporarily un-gated each page to confirm the actual form renders without runtime errors (including MudDatePicker on the foster form), then restored [Authorize]. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
SurrenderApply.razor (/surrender/apply) posts to RequestDogSurrender (dog name, breed, age in months, reason, temperament notes, health notes - no enums in this request, all plain text/numeric fields). Same pattern as Volunteer/Foster: no member-facing status lookup exists for surrender requests (review is Admin-only via GetSurrenderReviewQueue), so the page shows a plain success message with no link. Nav link added. Verified live: anonymous access 302-redirects to /login; temporarily un-gated the page to confirm all 6 fields render correctly with no server errors, then restored [Authorize]. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Connected the full local stack to a real Supabase project for the first
time and fixed everything that broke - all previously speculative/
"unverified this session" per the code's own prior comments:
- Api.Host validated JWTs against a hardcoded HS256 SymmetricSecurityKey,
but real Supabase projects sign session tokens with ES256/JWKS. Fixed
to use Authority-based OIDC discovery (Supabase exposes a real
/auth/v1/.well-known/openid-configuration document) so ASP.NET Core's
JwtBearer handler fetches/caches/rotates the signing key itself - no
manual key material in config at all anymore.
- VerifiedOwner/Admin/Shelter policies all checked RequireClaim(
"email_verified", "true"), but a real Supabase JWT has no such
top-level claim - it's nested inside the "user_metadata" claim as JSON
({"email_verified":true}). New EmailVerifiedRequirement/
EmailVerifiedAuthorizationHandler (BuildingBlocks.Web) parses it
correctly. This affected every VerifiedOwner-gated endpoint in the
app, not just one page.
- Login.razor/Register.razor rendered a duplicate __RequestVerificationToken
hidden field - EditForm(method="post") already renders one
automatically, and an explicit <AntiforgeryToken /> rendered a second
identical one, breaking real browser form submission (curl-based
testing never caught this since manually-built POST bodies only ever
included one token).
- Blazor.App's Data Protection keys were ephemeral (regenerated on every
restart), silently invalidating outstanding antiforgery tokens/auth
cookies. Now persisted to disk (.dataprotection-keys/, gitignored).
ADR-030 added: dotnet user-secrets is now the local-dev secrets store
for both Api.Host and Blazor.App (UserSecretsId added to both), not
appsettings.Development.json - safer (genuinely outside the repo) and
avoids the precedence trap discovered this session (a stale user-secrets
file from an earlier session silently overrode every
appsettings.Development.json edit for hours before being found).
GETTING_STARTED.md updated to match, plus the IPv6-only Direct
Connection gotcha and the Database-Webhooks-don't-fire-retroactively
gotcha, both discovered live.
Verified end-to-end for real (not just anonymous-redirect checks): real
signup, real email confirmation, real login, real JWT validation, real
authorization, real OwnerAccount data rendered in My Account.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
After reviewing 6 real shelter/rescue charity sites (Dogs Trust Ireland, MADRA, RSPCA, ISPCA/NSPCA, Mission K9 Rescue, Blue Cross), decided to descope the product away from its original "dog dating app" framing toward a shelter/rescue/foster/adoption operations tool plus non-profit administration. Cut: Discovery/Matching, Chat, Places, Moderation, Community, Providers, Shop. Merged: Profiles -> ShelterAdoption. Kept as-is: Identity, ShelterAdoption, Notifications, Media, Admin. Kept on roadmap: Content, Lost & Found. Repurposed: Scheduling (shelter ops instead of its original unclear scope). New: Donations/Sponsorship. This commit only records the decision - a SCOPE NOTE header in Spec/K9CRUSH.emlang.v3.yaml and a superseded-pointer at the top of docs/04-high-level-design.md. No code, tests, or schemas touched or deleted - that's a separate, not-yet-started pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Executes the descope decision recorded in Spec/K9CRUSH.emlang.v3.yaml's
SCOPE NOTE and module-boundaries memory: cuts the swipe/matching
mechanic, user-to-user chat, place reviews/meetups, and content
moderation entirely, since none of these appeared on any of the 6 real
shelter/rescue charity sites reviewed, and they were core to the
product's original "dog dating app" framing this pivot moves away from.
Removed: src/Modules/{Discovery,Chat,Places,Moderation} (Domain/Contracts/
Api), tests/K9Crush.Modules.{Discovery,Moderation,Places}.Tests, and
tests/K9Crush.IntegrationTests/{Chat,Discovery,Moderation}.
Severed the two real cross-module dependencies found via a dedicated
investigation pass before starting:
- Notifications' NotifyOnMatch automation (consumed Discovery's
MatchCreatedV1) - deleted along with its test and the Discovery.Contracts
project reference.
- Media's RemoveMediaOnContentRemovalRequested automation (consumed
Moderation's ContentRemovalRequestedV1) - deleted along with its test
and the Moderation.Contracts project reference; the now-orphaned
media.integration-events queue registration removed too since nothing
else in Media needs a cross-module queue. MediaContentFlaggedV1 (Media's
own event, previously consumed by Moderation) is left in place - Report
Media is a reasonable standalone feature independent of Moderation's
removal, just currently has no consumer - disclosed, not silently
dropped.
Updated Program.cs (module registration array, using directives, the two
Marten-domain-event SubscribeToEvent<T> registrations that only Discovery/
Chat used), Api.Host.csproj, K9Crush.sln (via `dotnet sln remove`), and
all three K9Crush.ArchitectureTests fitness test files (ModuleBoundaryTests,
EntitySerializationFitnessTests, HandlerNamingFitnessTests) plus their
csproj - each of these fitness tests reflects over every module's
assemblies by name, so they needed the same five-module list trimmed
that everything else did.
Verified: full solution builds clean, all 7 non-integration test
assemblies pass (393 tests: Admin 11, Profiles 16, Identity 45,
ArchitectureTests 121, ShelterAdoption 169, Notifications 20, Media 11).
Known follow-ups, not yet done (separate commits):
- Home.razor still calls the now-deleted /api/v1/discovery/feed endpoint
at runtime (compiles fine, breaks on click) - replacing with a static
landing page per user's explicit choice.
- Profiles module (DogProfile/AddDogProfile wizard) still exists
standalone - merging it into ShelterAdoption with photo-attachment
support ported onto DogListing is a separate, larger design task.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Home.razor called the now-deleted /api/v1/discovery/feed endpoint at runtime (compiled fine, would 404 on click) - Discovery was removed in the previous commit as part of the product's descope. Replaced with a static landing page (no API call, no rendermode needed): a welcome message plus four action cards linking to the journeys that actually matter now - Browse Adoptable Dogs, Apply to Foster, Apply to Volunteer, Surrender a Dog. Nav label changed from "Discover" to "Home" to match - "Discover" was named for the swipe-feed framing that's gone now. Verified live: 200 OK, all four cards render with correct hrefs, no server errors. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ge 3/3) Removes the Profiles module entirely - its "member's own dog" dating profile concept is moot now that Discovery (its only consumer) is gone. The one capability worth keeping, photo attachment, is ported onto DogListing directly: new PhotoIds field + AttachPhoto method, and a new AddDogListingPhoto slice (ownership-checked the same way EditDogListing is). GetAdoptionListings/GetDogListingDetails now surface PhotoIds too. Completes the module cut list from the 2026-07-23 descope decision - Chat/Discovery/Moderation/Places were removed in stage 1, Home page fixed in stage 2, Profiles merged here in stage 3. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…odel (Phase 0/5) Establishes the governance/tooling foundation for retrofitting all 5 live modules (Identity, ShelterAdoption, Notifications, Media, Admin) from plain Marten document-store to event-sourced, per an explicit product decision that event sourcing is the whole point of this app. - ADR-031 recorded in docs/03-solution-architecture.md; Section 2.1's module-classification table and the ADR-019 worked example (previously citing the deleted Discovery/MatchAggregate) both updated. - docs/05-event-modeling-blueprint.md's mechanical code samples, folder convention, and fitness-test section updated for the FetchForWriting/ AggregateStreamAsync + self-aggregating dual-use snapshot pattern. - New CommandStateFitnessTests.cs: a Mono.Cecil IL-scan catching a command handler that accidentally LoadAsync/Query's a snapshot-registered type instead of using FetchForWriting - verified via a real self-test using actual Marten types, not asserted. Required walking into compiler-generated async state-machine nested types, since the naive method-level scan missed calls hidden behind await. - TestingApproach.md updated for the new default, including a correction: FetchForWriting/FetchForExclusiveWriting were initially assumed unmockable (misread of an unrelated internal type named FetchForWritingExtensions) - confirmed via reflection + a working NSubstitute spike that they're real IEventStoreOperations interface members, mockable exactly like LoadAsync. Layer 2 does not shrink because of this retrofit. - Global Marten.Exceptions.ConcurrentUpdateException -> 409 mapping in Program.cs (name corrected the same way - not ConcurrencyException). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
--- updated-dependencies: - dependency-name: FluentAssertions dependency-version: 8.10.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com>
Media is the proof-of-concept module for the event-sourcing retrofit - smallest surface (1 entity, 4 command handlers, no read models). Validates the whole pattern end-to-end against real Postgres before the harder modules (Notifications' lock fusion, Identity's grace-period saga, ShelterAdoption's cross-stream writes) build on top of it. - MediaAsset becomes a self-aggregating event-sourced entity (Create/Apply over MediaAssetUploadedV1/SharedV1/RemovedV1). RemoveMediaHandler's old genuine session.Delete() becomes a MediaAssetRemovedV1 flag - streams can't be hard-deleted. - ReportMediaHandler (read-only, never mutates the asset) uses a minimal ADR-019-compliant ReportMediaState via AggregateStreamAsync, not FetchForWriting - it has nothing to append. - MediaModule.cs registers the event stream only; no Inline snapshot, since nothing under ReadModels/** queries MediaAsset today. Two things found only by running real code against Testcontainers Postgres (neither visible from mocks or compile-time checks alone), both corrected in Program.cs and this phase's own commit: - Every retrofitted entity's .Domain project needs a direct Marten PackageReference, not just .Api - Marten 9's Create/Apply dispatch is a compile-time source generator with no runtime fallback, and it only runs in an assembly that references the package. - FetchForWriting/AppendOne concurrency conflicts throw JasperFx.Events.EventStreamUnexpectedMaxEventIdException (base: JasperFx.ConcurrencyException) - a different hierarchy from Marten.Exceptions.ConcurrentUpdateException, which is Marten's document- level (Store()) concurrency exception. Two earlier guesses at this type name were wrong before this was confirmed by actually racing two sessions against the same stream in a Testcontainers spike test. Also confirmed live, matching what was assumed: FetchForWriting/ AggregateStreamAsync return null on a nonexistent stream rather than throwing - every handler's NotFound branch is correct as designed. FetchForWriting/AppendOne/AggregateStreamAsync are all genuine IEventStoreOperations interface members (confirmed via reflection + a working NSubstitute spike during Phase 0) - Layer 2 tests mock them exactly like LoadAsync/Store always worked, no Layer 3 fallback needed for Media's command handlers. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Admin proves two mechanics Media (Phase 1) didn't need: a stream whose first event comes from a cross-module automation trigger rather than a local command, and the dual-use Inline-snapshot pattern for a genuinely queried read model. - FeedbackInboxItem becomes self-aggregating (Create/Apply over FeedbackInboxItemCreatedV1/RespondedV1/ResolvedV1). Its stream id is externally supplied (Identity's own FeedbackId), not freshly generated - the same wrinkle planned for Identity's Phase 4, encountered here first. - FeedbackSubmittedProjectorHandler (an automation reacting to Identity's cross-module FeedbackSubmittedV1) used to rely on Store()'s upsert semantics for safe at-least-once redelivery. Event streams don't upsert - StartStream on an existing id throws - so it now checks via AggregateStreamAsync first and no-ops if the stream already exists. - GetFeedbackInboxHandler/GetFeedbackDetailHandler genuinely query FeedbackInboxItem (Query<T>/LoadAsync), so unlike Media's MediaAsset it IS registered as its own Inline snapshot in AdminModule.cs - confirmed working for real via Postgres (6 Layer 3 tests, including the existing GetFeedbackInboxIntegrationTests seeding through real StartStream calls instead of a raw Store()). Added to CommandStateFitnessTests.cs's snapshot watchlist. Both Phase 1 findings applied without rediscovery: Admin.Domain now references Marten directly (source-generator dispatch), and the global 409 mapping already covers the real JasperFx.ConcurrencyException hierarchy. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ase 3/5) Notifications proves the module has genuinely different entity shapes under one roof, and settles the lock-fusion design question flagged when the retrofit plan was written. - NotificationTemplate: Locked/LockedByOwnerId stay genuine domain state fused into NotificationTemplateEditedV1 (one event, matching the pre-retrofit Edit() method exactly) - not replaced by Marten's session-level FetchForExclusiveWriting lock, since Locked is displayed in ViewNotificationTemplatesHandler's read model and a session-scoped lock is invisible to a second session's read. Registered as its own Inline snapshot for that same read model. - NotificationPreference: event-sourced (Create/Apply over Created/ UpdatedV1), also Inline-snapshotted for ViewNotificationPreferencesHandler. NotificationDispatcher's plain LoadAsync read of it (used by every Notify* automation) is not an ADR-019 violation - Dispatcher never mutates NotificationPreference or decides its validity, only reads a fact from it to inform an unrelated write, the same shape ApproveApplicationHandler already uses reading ShelterAccount. - NotificationLog: event-sourced, create-only (no Apply overloads at all - nothing ever mutates a log entry), no snapshot (nothing queries it). - OwnerContact: deliberately stays a plain document, not event-sourced - a pure cross-module denormalized cache with no domain transitions of its own to capture as events. - UpdateNotificationPreferencesHandler's lazy-create-on-first-update branch (FetchForWriting comes back with a null Aggregate -> StartStream with both the default-created event and the immediate update event together, since a null-Aggregate handle can't itself be appended to) is verified against real Postgres in a new integration test, not assumed - confirms both the fresh-stream and already-exists branches behave correctly across separate sessions. Both Phase 1 findings applied without rediscovery: Notifications.Domain now references Marten directly, and the existing global 409 mapping already covers whatever this module throws. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Identity proves a caller-supplied (Supabase) stream id works identically to a freshly-generated one, and surfaces a real gap in the fitness test's own design. - OwnerAccount: self-aggregating (Create/Apply over 9 event types matching its 9 domain methods 1:1). Registered as its own Inline snapshot - OwnerAccountView/ViewProfileSettings query it, and MartenOwnerRoleLookup (ADR-017, read on every Admin/Shelter-policy-gated request) does too. Domain events named OwnerAccountXxxV1 throughout specifically to avoid colliding with this module's own Contracts events of similar names (e.g. Contracts.AccountDeletionRequestedV1). - Feedback: event-sourced, create-only (no Apply overloads), same shape as Notifications' NotificationLog (Phase 3). - ProvisionOwnerOnSupabaseSignupHandler (Supabase webhook automation) gets the same idempotent-redelivery treatment as Admin's FeedbackSubmittedProjectorHandler (Phase 2): AggregateStreamAsync existence check before StartStream, since redelivery can no longer rely on Store()'s upsert semantics. - The account-deletion grace-period saga (Request -> Confirm -> scheduled CheckAccountGracePeriodExpired -> re-check -> PermanentlyDelete) keeps its exact re-check idempotency shape under FetchForWriting/AppendOne - no behavioral change, just the persistence mechanism underneath. Found and fixed a real gap in Phase 0's CommandStateFitnessTests.cs: BootstrapAdminHandler's session.Query<OwnerAccount>().AnyAsync(x => x.Role == Admin) - a legitimate cross-population check (does ANY OwnerAccount have Role Admin, checked before separately FetchForWriting-ing the CALLER's own account) - was flagged as a violation, since the IL scanner can't distinguish a population query from a self-load. Resolved with a narrow, hand-reviewed allowlist (ReviewedCrossPopulationQueryExceptions) for Query<T>() specifically - LoadAsync<T>() against a snapshot type stays unconditionally flagged, since a command always has FetchForWriting/AggregateStreamAsync available for a genuine by-id load. ShelterAdoption's SubmitApplicationHandler (Phase 5) will need the same treatment for its own cross-population Query<Application>() check. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…Phase 5/5, production code) All 6 entities (ShelterAccount, DogListing, Application, DogSurrenderRequest, FosterApplication, VolunteerApplication) converted to self-aggregating Create/Apply event-sourced entities with Inline snapshots; handlers moved to FetchForWriting/StartStream/AppendOne. Includes the no-hard-delete flag pattern for DogListing (IsRemoved) and the two genuine multi-stream writes (ApproveApplicationHandler, AcceptDogSurrenderHandler). Test suite rewrite for the new entity API surface is still in progress (tracked separately) - this commit is production code only, plus the CommandStateFitnessTests watchlist/allowlist entries for this module's entities and cross-population queries.
…Phase 5/5, test suite) Rewrites all ShelterAdoption Domain/Handlers unit tests and Layer 3 integration tests for the new self-aggregating entity API (RequestNew/ AddNew/SubmitNew/StartDraftNew/ApplyNew tuple factories, FetchForWriting/ AppendOne/StartStream in place of LoadAsync/Store). Adds MartenEventStoreTestHelpers.cs (mirrors the helper already used by Media/ Admin/Notifications/Identity). Also extends CommandStateFitnessTests.cs with a new ReviewedCrossEntityLoadExceptions allowlist: unlike earlier phases, ShelterAdoption's 6 entities are densely cross-referenced and all Inline-snapshotted, so legitimate read-only LoadAsync calls against a DIFFERENT aggregate (e.g. an ownership check against ShelterAccount from a handler mutating DogListing) were indistinguishable from a genuine self-load to the IL scanner. Each entry is reviewed and documents which entity is mutated vs. which different entity is read. Full solution now builds and tests green: 176 ShelterAdoption unit tests, 116 architecture fitness tests, 39 integration tests (25 ShelterAdoption), plus all other modules unaffected. This completes the "full retrofit of all 5 live modules to event sourcing" directive (ADR-031) - Media, Admin, Notifications, Identity, and now ShelterAdoption are all event-sourced.
Generic template forked from build-kit-dotnet after K9Crush's ADR-031 retrofit proved out "event-sourced everywhere, Inline snapshots only where queried" as the right default. Removes the document-store branch from build-state-change/build-state-view/build-automation, reconciles ADR-019's live-computed decision state with ADR-031's Inline snapshots as two distinct rules rather than a contradiction, and genericizes paths/naming so the kit can be dropped into any Wolverine.Http + Marten + RabbitMQ .NET project. Untested against a real board/solution so far.
Captures the Wolverine/Marten package combo and host setup worked out from a real dotnet build, since the skills assume this wiring exists without ever defining it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
--- updated-dependencies: - dependency-name: Mono.Cecil dependency-version: 0.11.6 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com>
--- updated-dependencies: - dependency-name: xunit dependency-version: 2.9.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com>
--- updated-dependencies: - dependency-name: xunit.runner.visualstudio dependency-version: 3.1.5 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com>
Brought in from outside the repo for convenience; not part of the tracked project. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…-kit-dotnet-es Ports the orchestrate.mjs setup from the sibling PawMatch project: picks a board chapter, retrofits SLICE_BORDER markers onto columns that predate slices, flips them Planned, and runs N Ralph instances in parallel git worktrees (avoiding the file-clobbering races a shared working tree causes) until every slice reaches a terminal state. Also brings over the reliability fixes that make unattended/parallel Ralph runs safe (capped retry + auto-Blocked on repeated failure, maxSlicesPerRun, per-slice budget cap, a prompt-path fix for the kit's actual location) and the Phase 1 pre-checkin quality gate (build/format/vulnerable-package/secret checks + a stuck-loop guard), wired into the K9Crush solution's own Claude Code settings. Along the way: build-kit-dotnet-es's own runtime state (.slices/, tasks.json, progress.txt, logs) was never gitignored — only the older build-kit-dotnet's was. Fixed. quality-checks.md is genericized for this project's actual architecture (RabbitMQ, ADR-031's unconditional event sourcing) rather than carrying over PawMatch-specific ADR references that don't apply here. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Every module's IMartenModuleConfiguration.Configure() independently set options.Events.DatabaseSchemaName on the one shared StoreOptions instance - a single store-wide setting, not per-module (confirmed against Marten's own docs), so whichever module ran last (Media) was silently winning for every module's event streams. Separately, none of the Inline-snapshot documents outside Notifications' OwnerContact had the per-type options.Schema.For<T>().DatabaseSchemaName() call Marten actually requires, so they defaulted to Postgres's public schema - briefly a real exposure window given that's the schema Supabase's PostgREST auto-exposes. Fix: event store schema is now set once, centrally, in Program.cs (eventstore, shared by design); every module now explicitly scopes its own Inline-snapshot documents to its own schema. ADR-003 updated from Proposed to Decided with the corrected documents-vs-events split. Found live while seeding activity into the real Supabase project after a Supabase inactivity warning; existing 2026-07-30 test data was migrated into the correct schemas as part of this fix, not left behind or discarded. Full test suite (422 tests, including 39 real-Postgres integration tests) passing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ed snapshot Board and yaml were out of sync in both directions: removed the 18 cut-scope chapters (Discovery/Chat/Places/Moderation/Community/Providers/ Shop/Profiles) that the 2026-07-23 descope decided but never executed in this file, added the 6 chapters that existed only on the board (BookingAppointments, VolunteerShiftRoster, MakingADonation, ManagingARecurringDonation, HandlingDataSubjectRequests, ErasureCascade), and split SurrenderingYourDog into the real Simple/[BUILT] flow plus a new SurrenderingYourDogFullIntake [PLANNED] chapter (per-shelter config, matching a corresponding board-side split). Renamed K9CRUSH.emlang.v3.yaml -> K9CRUSH.emlang.v3.2026-07-31.yaml since this file is now treated as a dated snapshot of the board rather than an incrementing version - future re-syncs replace it with a new dated file. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
mergeWorktrees() trusted a self-reported board sliceStatus as the sole completion signal, then blindly retried `git worktree remove --force` on ANY removal failure - including the case where a Ralph loop had already picked up a new Planned slice (getFirstPlannedSlice pulls from a shared, un-chapter-scoped .slices/ index) and left uncommitted work in the worktree the instant its tracked slice went terminal. Gate removal on `git status --porcelain` instead: a dirty worktree is left in place with manual-cleanup instructions rather than force-deleted. Also add a best-effort .ralph-stop signal file, dropped into a worktree right before merging, that the ralph loop checks each iteration to stop picking up new planned-slice work - narrows the race window, though the git-status gate is the real backstop since it can't interrupt in-flight work. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…caffold/K9Crush/FluentAssertions-8.10.0 Bump FluentAssertions from 6.12.1 to 8.10.0
…scaffold/K9Crush/Mono.Cecil-0.11.6 Bump Mono.Cecil from 0.11.3 to 0.11.6
…scaffold/K9Crush/xunit-2.9.3 Bump xunit from 2.9.2 to 2.9.3
…/code/K9Crush-scaffold/K9Crush/xunit.runner.visualstudio-3.1.5 # Conflicts: # code/K9Crush-scaffold/K9Crush/Directory.Packages.props
…scaffold/K9Crush/xunit.runner.visualstudio-3.1.5 Bump xunit.runner.visualstudio from 2.8.2 to 3.1.5
WolverineFx/.Http/.Marten/.RabbitMQ/.RuntimeCompilation to 6.23.1, Marten/ Marten.AspNetCore to 9.20.1 - applied as one manual bump instead of merging Dependabot PRs #24/#27/#28/#29/#30 individually, since those packages release in lockstep (per this file's own existing comment) and the 5 PRs disagreed with each other on shared version lines (#24 vs #28 both touched Marten to different targets; #27/#29/#30 all touched WolverineFx independently). Verified: solution builds clean, all 383 non-integration tests pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
Re-enabling both after their run-duration-driven disable, but only for main - dev is pushed to directly and frequently, which is why they were disabled in the first place.
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.
Summary
Spec/K9CRUSH.emlang.v3.2026-07-31.yamlsynced with the eventmodelers board (cut-scope chapters removed, board-only chapters added, Surrendering Your Dog split into Simple/Full Intake)fix: correct Marten schema isolation for events and documentsfeat: add multi-instance chapter orchestrator + quality gate to build-kit-dotnet-es, plus a follow-up fix preventing the orchestrator from force-destroying uncommitted Ralph work on cleanupTest plan
dotnet buildclean (0 errors)🤖 Generated with Claude Code