diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 07a4251..ef58b82 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,14 +1,14 @@ name: CI -# Runs on PRs targeting dev/main (the pre-merge gate) and on direct -# pushes to dev/main (this repo's actual workflow so far has been -# committing straight to dev - this re-runs the same checks as a -# post-merge confirmation so nothing slips through either path). +# Runs on PRs targeting main (the pre-merge gate for dev->main catch-up +# PRs) and on direct pushes to main (post-merge confirmation). dev itself +# is pushed to directly and frequently, so it's intentionally excluded - +# see cicd-setup memory for why CI was disabled in the first place. on: pull_request: - branches: [dev, main] + branches: [main] push: - branches: [dev, main] + branches: [main] env: DOTNET_NOLOGO: true diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index b558a4c..afd5629 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,14 +1,15 @@ name: CodeQL -# Security scanning (SAST). Runs on PRs/pushes to dev/main plus a weekly +# Security scanning (SAST). Runs on PRs/pushes to main plus a weekly # schedule so newly-disclosed vulnerability patterns get caught even on # code nobody's touched recently - this is GitHub's own recommended -# default cadence for CodeQL. +# default cadence for CodeQL. dev is excluded since it's pushed to +# directly and frequently - see cicd-setup memory. on: pull_request: - branches: [dev, main] + branches: [main] push: - branches: [dev, main] + branches: [main] schedule: - cron: "0 6 * * 1" diff --git a/Spec/K9CRUSH.emlang.v3.yaml b/Spec/K9CRUSH.emlang.v3.2026-07-31.yaml similarity index 58% rename from Spec/K9CRUSH.emlang.v3.yaml rename to Spec/K9CRUSH.emlang.v3.2026-07-31.yaml index 4af7f8a..f557adf 100644 --- a/Spec/K9CRUSH.emlang.v3.yaml +++ b/Spec/K9CRUSH.emlang.v3.2026-07-31.yaml @@ -69,26 +69,39 @@ # core, plus training/advice content and (for larger ones) multi-centre # management. # -# Modules/chapters this cuts (not yet removed from this file or the -# codebase - this note records the *decision*, a later pass does the -# actual chapter-by-chapter removal and code deletion): -# - Discovery/Matching (the swipe mechanic itself) - e.g. SwipingAndMatching -# - Chat - e.g. MessagingDirectGroup, ChatMatchTriggeredMessaging -# - Places (dog-park/cafe reviews, meetups) - e.g. ClaimABusinessListing, -# LeaveAReviewRestaurantOrDogPark -# - Moderation (ModeratingFlaggedContentUserReports) - nothing left to +# Modules/chapters this cuts - DONE, both in the codebase (Discovery, Chat, +# Places, Moderation deleted 2026-07-24, see module-boundaries memory) and +# in this file (chapters removed 2026-07-31, matching the eventmodelers +# board which never carried them past this decision either): +# - Discovery/Matching (the swipe mechanic itself) - was SwipingAndMatching, +# TheWindowShopper, and the swipe/match half of ManagingSavedDogsSpots +# - Chat - was MessagingDirectGroup, ChatMatchTriggeredMessaging +# - Places (dog-park/cafe reviews, meetups) - was ClaimABusinessListing, +# LeaveAReviewRestaurantOrDogPark, TheSocialDogWalker, RSVPingDirectly, +# TheEventBrowser, and the "Spots" half of ManagingSavedDogsSpots +# - Moderation - was ModeratingFlaggedContentUserReports - nothing left to # moderate once the above social/UGC surfaces are gone -# - Community (social feed/follow) - e.g. ActivityFeed, FollowAProfile -# - Providers (vendor marketplace) - e.g. DogServiceProviderApplication, +# - Community (social feed/follow) - was ActivityFeed, FollowAProfile, +# ArrangeAPlaydate +# - Providers (vendor marketplace) - was DogServiceProviderApplication, # ContactADogServiceProvider, ReviewingServiceProviderApplications -# - Shop - e.g. ShopVendorApplication, TheGiftShopper (every reference +# - Shop - was ShopVendorApplication, TheGiftShopper (every reference # site outsources this to an external platform rather than building # it in-house) # -# Profiles (e.g. AddDogProfile) is being merged into ShelterAdoption - a -# dog's photo/breed/bio lives on DogListing now, no separate "dating +# Profiles (was AddDogProfile, also removed) merged into ShelterAdoption - +# a dog's photo/breed/bio lives on DogListing now, no separate "dating # profile" concept needed once there's nothing to swipe on. # +# NOT cut despite living under a similarly-named old persona/tag: +# TheCuriousNewDogOwner and TheSkepticalParent are kept for now (generic +# guest-marketing-funnel chapters, no single cut module owns their content) +# though both are visibly stale against the real rebuilt Home.razor +# (welcome message + Adopt/Foster/Volunteer/Surrender cards) - worth a +# deliberate look, not silently deleted as part of this pass. TheUrgentSearch +# is also kept - despite its old board tag, its content (Lost Dog Listing, +# Sighting Report, Notify Dog Owner) is real Lost & Found scope, not cut. +# # Kept as-is: Identity, ShelterAdoption, Notifications, Media, Admin. # Kept on the roadmap, not yet built: Content (training/advice - every # reference site has this), Lost & Found (validated by MADRA/ISPCA). @@ -190,116 +203,6 @@ slices: - c: Member/Confirm Profile then: - e: Member/Profile Confirmed - # [PARTIAL] Discovery module. "Preview Nearby Dogs" is a direct state-view - # query (GetDiscoveryFeedHandler, no [Authorize] - Guests browse too), - # not a separate command+event pair - same "page-load command+event - # collapses into the view itself" consolidation applied to every - # read-model in this codebase. - # DEVIATION: matchType is hardcoded "dog_to_dog" in the real response - - # the "shelter_dog" variant (surfacing ShelterAdoption's DogListings in - # this same feed) is an acknowledged, disclosed gap, not built. - # DEVIATION: the Guest sign-up bridge (Initiate Sign-Up -> Confirmation - # Email Sent -> Confirm Profile, plus v1's own "Prompt Sign-Up" screen) - # is NOT implemented as commands in this codebase at all - Supabase - # Auth owns the entire signup/login/confirmation lifecycle directly - # (ADR-005); Identity only reacts after the fact via - # ProvisionOwnerOnSupabaseSignup/VerifyOwnerOnSupabaseConfirmation - # webhook automations (see the new BootstrappingTheFirstAdmin chapter). - # Kept here as steps with no working command behind them, matching v1's - # intent, since the screens are still real - only the commands are - # external. "Reject Claim (Match No Longer Available)" is not a - # separate command either - it's the Conflict branch of - # ClaimSavedMatchHandler/FlagDogOfInterestHandler when the dog is no - # longer in the feed, both calling the exact same DogOfInterest.Flag(...) - # under the hood regardless of which of these two narrative entry - # points triggered it. - TheWindowShopper: - steps: - - v: Guest/Nearby Dogs Preview - props: - dogProfileId: dog_204 - name: Luna - breed: Labrador - distanceMiles: '1.2' - matchType: dog_to_dog - - t: Guest/Nearby Dogs - - c: Guest/Block Match Attempt - props: - dogId: dog_482 - - e: Guest/Match Attempt Blocked - props: - dogId: dog_482 - reason: sign_up_required - - t: Guest/Sign Up to Match - - c: Member/Initiate Sign-Up - - e: Member/Sign-Up Initiated - - c: Member/Send Confirmation Email - - e: Member/Confirmation Email Sent - - t: Member/Confirm Profile - - c: Member/Confirm Profile - - e: Member/Profile Confirmed - - t: Member/Saved Match - - c: Member/Claim Saved Match - - e: Member/Saved Match Claimed - props: - dogId: dog_482 - - c: Member/Flag Dog Of Interest - - e: Member/Dog Of Interest Flagged - - t: Member/Match No Longer Available - - e: 'Member/Saved Match No Longer Available' - props: - reason: 'Saved Match No Longer Available.' - tests: - NearbyDogsPreview: - then: - - v: Guest/Nearby Dogs Preview - MatchAttemptBlocked: - when: - - c: Guest/Block Match Attempt - then: - - e: Guest/Match Attempt Blocked - SignUpInitiated: - given: - - e: Guest/Match Attempt Blocked - when: - - c: Member/Initiate Sign-Up - then: - - e: Member/Sign-Up Initiated - ConfirmationEmailSent: - given: - - e: Member/Sign-Up Initiated - when: - - c: Member/Send Confirmation Email - then: - - e: Member/Confirmation Email Sent - ProfileConfirmed: - given: - - e: Member/Confirmation Email Sent - when: - - c: Member/Confirm Profile - then: - - e: Member/Profile Confirmed - SavedMatchClaimed: - given: - - e: Member/Profile Confirmed - when: - - c: Member/Claim Saved Match - then: - - e: Member/Saved Match Claimed - DogOfInterestFlagged: - given: - - e: Member/Profile Confirmed - when: - - c: Member/Flag Dog Of Interest - then: - - e: Member/Dog Of Interest Flagged - SavedMatchNoLongerAvailable: - given: - - e: Member/Profile Confirmed - when: - - c: Member/Claim Saved Match - then: - - e: 'Member/Saved Match No Longer Available' # [PARTIAL] ShelterAdoption module. Browse/Detail are direct state-view # queries (GetAdoptionListingsHandler/GetDogListingDetailsHandler, no # per-shelter filter - every DogListing across every active shelter), @@ -661,137 +564,6 @@ slices: - c: Member/Submit Application then: - e: Member/Application Submitted - ClaimABusinessListing: - steps: - - t: Business Owner/Select Listing & Enter Contact Info - - c: Business Owner/Request Listing Claim - - e: Business Owner/Listing Claim Requested - - t: Business Owner/Verification Pending - - c: Business Owner/Send Claim Verification - - e: Business Owner/Claim Verification Sent - - c: Business Owner/Verify Claim - - e: Business Owner/Claim Verified - - t: Business Owner/Verification Issues - - c: Business Owner/Flag Claim Verification Issues - - e: Business Owner/Claim Verification Issues Found - - c: Business Owner/Manually Approve Claim - - e: Business Owner/Claim Manually Approved - - c: Business Owner/Reject Claim - - e: Business Owner/Claim Rejected - - t: Business Owner/Claim Approved — Ownership Transferred - - c: Business Owner/Transfer Listing Ownership - - e: Business Owner/Listing Ownership Transferred - tests: - ClaimantSubmitsContactInfoForAnExistingListing: - when: - - c: Business Owner/Request Listing Claim - then: - - e: Business Owner/Listing Claim Requested - VerificationCodeSentAfterClaimRequest: - given: - - e: Business Owner/Listing Claim Requested - when: - - c: Business Owner/Send Claim Verification - then: - - e: Business Owner/Claim Verification Sent - ContactInfoMatchesClaimAutoVerified: - given: - - e: Business Owner/Listing Claim Requested - - e: Business Owner/Claim Verification Sent - when: - - c: Business Owner/Verify Claim - then: - - e: Business Owner/Claim Verified - ContactInfoDoesnTMatchFlaggedForManualReview: - given: - - e: Business Owner/Listing Claim Requested - - e: Business Owner/Claim Verification Sent - when: - - c: Business Owner/Flag Claim Verification Issues - then: - - e: Business Owner/Claim Verification Issues Found - AdminManuallyApprovesAFlaggedClaim: - given: - - e: Business Owner/Listing Claim Requested - - e: Business Owner/Claim Verification Sent - - e: Business Owner/Claim Verification Issues Found - when: - - c: Business Owner/Manually Approve Claim - then: - - e: Business Owner/Claim Manually Approved - AdminRejectsAFlaggedClaimWithAReason: - given: - - e: Business Owner/Listing Claim Requested - - e: Business Owner/Claim Verification Sent - - e: Business Owner/Claim Verification Issues Found - when: - - c: Business Owner/Reject Claim - then: - - e: Business Owner/Claim Rejected - OwnershipTransferredAfterAutoVerification: - given: - - e: Business Owner/Listing Claim Requested - - e: Business Owner/Claim Verified - when: - - c: Business Owner/Transfer Listing Ownership - then: - - e: Business Owner/Listing Ownership Transferred - ShopVendorApplication: - steps: - - t: Vendor/Sell in the Merch Store - - c: Vendor/Start Vendor Application - - e: Vendor/Vendor Application Started - - t: Vendor/Business & Product Details - - c: Vendor/Submit Application Details - - e: Vendor/Application Details Submitted - - t: Vendor/Upload Business Documents - - c: Vendor/Submit Vendor Documents - - e: Vendor/Vendor Documents Submitted - - t: Vendor/Vendor Application Approved - - c: Vendor/Approve Vendor Application - - e: Vendor/Vendor Approved - - t: Vendor/Vendor Application Rejected - - c: Vendor/Reject Vendor Application - - e: Vendor/Vendor Application Rejected - tests: - VendorStartsANewApplication: - when: - - c: Vendor/Start Vendor Application - then: - - e: Vendor/Vendor Application Started - BusinessAndProductDetailsSubmitted: - given: - - e: Vendor/Vendor Application Started - when: - - c: Vendor/Submit Application Details - then: - - e: Vendor/Application Details Submitted - BusinessDocumentsUploadedForReview: - given: - - e: Vendor/Vendor Application Started - - e: Vendor/Application Details Submitted - when: - - c: Vendor/Submit Vendor Documents - then: - - e: Vendor/Vendor Documents Submitted - VendorApplicationApproved: - given: - - e: Vendor/Vendor Application Started - - e: Vendor/Application Details Submitted - - e: Vendor/Vendor Documents Submitted - when: - - c: Vendor/Approve Vendor Application - then: - - e: Vendor/Vendor Approved - VendorApplicationRejectedUnreadableDocuments: - given: - - e: Vendor/Vendor Application Started - - e: Vendor/Application Details Submitted - - e: Vendor/Vendor Documents Submitted - when: - - c: Vendor/Reject Vendor Application - then: - - e: Vendor/Vendor Application Rejected # [BUILT] ShelterAdoption module (+Identity's PromoteOwnerToShelterOnAccountCreated # automation, triggered by ShelterAccountCreatedV1 - the only mechanism # by which OwnerRole.Shelter is ever reached). @@ -1024,188 +796,6 @@ slices: - e: Shelter Staff/Dog Listing Edited then: - e: Shelter Staff/Applicant Notified Of Listing Change - DogServiceProviderApplication: - steps: - - t: Service Provider/Start Provider Application - - c: Service Provider/Start Provider Application - - e: Service Provider/Provider Application Started - props: - serviceType: groomer | trainer | walker | minder | breeder - - t: Service Provider/Application Details - - c: Service Provider/Submit Application Details - - e: Service Provider/Application Details Submitted - - t: Service Provider/Sign Declaration - - c: Service Provider/Sign Declaration Form - - e: Service Provider/Declaration Form Signed - props: - applicableServiceTypes: breeder | trainer | groomer - declarationText: I agree to adhere to K9Crush platform rules - - c: Service Provider/Confirm Required Documents Submitted - - e: Service Provider/Required Documents Submitted - - t: Service Provider/Upload Insurance - - c: Service Provider/Upload Proof Of Insurance - props: - applicableServiceType: walker - optional: 'true' - - e: Service Provider/Proof Of Insurance Uploaded - props: - applicableServiceType: walker - optional: 'true' - - t: Service Provider/Approve Provider - - c: Service Provider/Approve Provider Application - - e: Service Provider/Provider Approved - props: - reviewedBy: K9Crush platform team - - t: Service Provider/Reject Provider - - c: Service Provider/Reject Provider Application - - e: Service Provider/Provider Application Rejected - - c: Service Provider/Flag Renewal Due - - e: Service Provider/Provider Renewal Due - props: - renewalPeriodDays: '365' - - t: Service Provider/Renew Verification - - c: Service Provider/Renew Verification - - e: Service Provider/Provider Verification Renewed - - c: Service Provider/Expire Verification (Not Renewed) - - e: Service Provider/Provider Verification Expired - - t: Service Provider/Resubmit Application - - c: Service Provider/Resubmit Provider Application - - e: Service Provider/Provider Application Resubmitted - - c: Service Provider/Re-review Provider Application - - e: Service Provider/Provider Reverified - tests: - ProviderApplicationStarted: - when: - - c: Service Provider/Start Provider Application - then: - - e: Service Provider/Provider Application Started - ApplicationDetailsSubmitted: - given: - - e: Service Provider/Provider Application Started - when: - - c: Service Provider/Submit Application Details - then: - - e: Service Provider/Application Details Submitted - DeclarationFormSigned: - given: - - e: Service Provider/Application Details Submitted - when: - - c: Service Provider/Sign Declaration Form - then: - - e: Service Provider/Declaration Form Signed - RequiredDocumentsSubmitted: - given: - - e: Service Provider/Declaration Form Signed - when: - - c: Service Provider/Confirm Required Documents Submitted - then: - - e: Service Provider/Required Documents Submitted - ProofOfInsuranceUploaded: - given: - - e: Service Provider/Application Details Submitted - when: - - c: Service Provider/Upload Proof Of Insurance - then: - - e: Service Provider/Proof Of Insurance Uploaded - ProviderApprovedFirstTry: - given: - - e: Service Provider/Required Documents Submitted - when: - - c: Service Provider/Approve Provider Application - then: - - e: Service Provider/Provider Approved - ProviderApprovedAfterFixAndReapply: - given: - - e: Service Provider/Provider Reverified - when: - - c: Service Provider/Approve Provider Application - then: - - e: Service Provider/Provider Approved - ProviderApplicationRejected: - given: - - e: Service Provider/Application Details Submitted - when: - - c: Service Provider/Reject Provider Application - then: - - e: Service Provider/Provider Application Rejected - ProviderRenewalDue: - given: - - e: Service Provider/Provider Approved - when: - - c: Service Provider/Flag Renewal Due - then: - - e: Service Provider/Provider Renewal Due - ProviderVerificationRenewed: - given: - - e: Service Provider/Provider Renewal Due - when: - - c: Service Provider/Renew Verification - then: - - e: Service Provider/Provider Verification Renewed - ProviderVerificationExpired: - given: - - e: Service Provider/Provider Renewal Due - when: - - c: Service Provider/Expire Verification (Not Renewed) - then: - - e: Service Provider/Provider Verification Expired - ProviderApplicationResubmitted: - given: - - e: Service Provider/Provider Application Rejected - when: - - c: Service Provider/Resubmit Provider Application - then: - - e: Service Provider/Provider Application Resubmitted - ProviderReverified: - given: - - e: Service Provider/Provider Application Resubmitted - when: - - c: Service Provider/Re-review Provider Application - then: - - e: Service Provider/Provider Reverified - ContactADogServiceProvider: - steps: - - c: Member/Browse Service Providers - - e: Member/Service Providers Browsed - props: - serviceType: groomer | trainer | walker | minder | breeder - - v: Member/Provider Reviews & Ratings - - t: Member/Browse Providers - - t: Member/Provider Details - - c: Member/Contact Provider - props: - providerVerified: 'true' - - e: Member/Provider Contacted - props: - providerVerified: 'true' - - t: Member/Message Provider - - c: Member/Send Message To Provider - - e: Member/Provider Message Sent - tests: - ServiceProvidersBrowsed: - when: - - c: Member/Browse Service Providers - then: - - e: Member/Service Providers Browsed - ProviderReviewsRatings: - given: - - e: Member/Service Providers Browsed - then: - - v: Member/Provider Reviews & Ratings - ProviderContacted: - given: - - e: Member/Service Providers Browsed - when: - - c: Member/Contact Provider - then: - - e: Member/Provider Contacted - ProviderMessageSent: - given: - - e: Member/Provider Contacted - when: - - c: Member/Send Message To Provider - then: - - e: Member/Provider Message Sent # [BUILT] Notifications module. "Open Notification Templates" is a # direct state-view query (ViewNotificationTemplatesHandler, all # templates, no filter), not a separate command+event. EditNotificationTemplateHandler @@ -1263,15 +853,6 @@ slices: - c: Shelter Staff/Save Notification Template then: - e: Shelter Staff/Notification Template Saved - ReviewingServiceProviderApplications: - steps: - - v: Admin/Application Review Queue - - t: Admin/Applications Queue - - t: Admin/Application Detail — Verification Evidence - tests: - QueueReflectsCurrentPendingApplications: - then: - - v: Admin/Application Review Queue ManagingTipsHealthContent: steps: - t: Admin/New Tip Editor @@ -1326,83 +907,6 @@ slices: - c: Admin/Unpublish Tip then: - e: Admin/Tip Unpublished - # [BUILT] Moderation module. "Content Flagged" is shared across 5 - # chapters on the original board (this one, MessagingDirectGroup's - # Report Message, ActivityFeed's Report Post, LeaveAReviewRestaurantOrDogPark's - # Report Review, and Media's own Report Media which never had a named - # chapter counterpart at all) - only Media (see - # UploadShareRemovePhotosAndVideos) and Places (LeaveAReviewRestaurantOrDogPark) - # actually produce it today; Chat/ActivityFeed's two remaining - # producers don't exist as real slices yet, so the queue only ever - # shows Media/Review content. - # DEVIATION: Suspend/Ban are gated by the yaml's OWN preconditions - # (Suspend needs a prior Warn, Ban needs a prior Warn AND Suspend) - - # confirmed those preconditions are real 409-Conflict guards in code, - # not just illustrative GWT framing. Warn/Dismiss/RemoveContent have no - # guard at all (valid from any status/state), also confirmed real. - # Deliberately does NOT enforce Suspend/Ban anywhere - these are - # recorded facts only (UserModerationRecord), never checked by any - # authorization policy. Real platform-wide enforcement is a separate, - # much larger, not-yet-built feature. - ModeratingFlaggedContentUserReports: - steps: - - v: Admin/Moderation Queue - props: - flagId: flag_501 - contentType: Media | Review - contentOwnerId: owner_44 - reporterOwnerId: owner_82 - status: Open - - t: Admin/Moderation Queue - - t: Admin/Flagged Content Detail - - c: Admin/Dismiss Flag - - e: Admin/Flag Dismissed - - c: Admin/Remove Content - - e: Admin/Content Removed - props: - cascadedTo: Media or Places (ContentRemovalRequestedV1) - - c: Admin/Warn User - - e: Admin/User Warned - props: - warningCount: '1' - - c: Admin/Suspend User - - e: Admin/User Suspended - - c: Admin/Ban User - - e: Admin/User Banned - tests: - QueueReflectsCurrentFlaggedContentAndReports: - then: - - v: Admin/Moderation Queue - AdminDismissesAFlagWithNoActionNeeded: - when: - - c: Admin/Dismiss Flag - then: - - e: Admin/Flag Dismissed - AdminRemovesContentThatViolatesPolicy: - when: - - c: Admin/Remove Content - then: - - e: Admin/Content Removed - AdminWarnsAUserForAFirstViolation: - when: - - c: Admin/Warn User - then: - - e: Admin/User Warned - RepeatOffenderSuspendedAfterAPriorWarning: - given: - - e: Admin/User Warned - when: - - c: Admin/Suspend User - then: - - e: Admin/User Suspended - RepeatOffenderBannedAfterWarningAndSuspension: - given: - - e: Admin/User Warned - - e: Admin/User Suspended - when: - - c: Admin/Ban User - then: - - e: Admin/User Banned # [BUILT] Admin module. Fed by Identity's FeedbackSubmittedV1 (see # AccountProfileSettings' "Submit Feedback" - the actual submission # entry point lives in Identity, not here; this chapter is entirely the @@ -1547,129 +1051,6 @@ slices: - c: Member/Report Media then: - e: Member/Content Flagged - TheSocialDogWalker: - steps: - - c: Member/View Map - props: - locationSource: device_geolocation | manual_city_country - location: Portland, OR - - e: Member/Map Viewed - props: - locationSource: device_geolocation | manual_city_country - location: Portland, OR - - v: Member/Nearby Spots - props: - spotId: spot_12 - name: Riverside Dog Park - type: park | cafe - trending: 'true' - offLeashArea: true (parks only) - dogMenuItems: Pup cup, dog biscuit (cafes only) - - t: Member/Map - - t: Member/Park Details - - c: Member/View Park Details - - e: Member/Park Details Viewed - - t: Member/Cafe Details - - c: Member/View Cafe Details - - e: Member/Cafe Details Viewed - - t: Member/Trending Spot - - c: Member/View Trending Spot - - e: Member/Trending Spot Viewed - - t: Member/Spot Reviews - - c: Member/View Spot Reviews - - e: Member/Spot Reviews Viewed - - t: Member/Sign Up to Review - - c: Member/Attempt To Leave Review - - e: Member/Review Attempt Gated - - t: Member/Sign Up - - c: Member/Initiate Sign-Up - - e: Member/Sign-Up Initiated - - c: Member/Send Confirmation Email - props: - linkExpiresInHours: '24' - - e: Member/Confirmation Email Sent - props: - linkExpiresInHours: '24' - - t: Member/Confirm Profile - - c: Member/Confirm Profile - - e: Member/Profile Confirmed - - t: Member/Write Review - - c: Member/Post Review - - e: Member/Review Posted - tests: - UserViewsTheMap: - when: - - c: Member/View Map - then: - - e: Member/Map Viewed - NearbySpots: - given: - - e: Member/Map Viewed - then: - - v: Member/Nearby Spots - ParkDetailsViewed: - given: - - e: Member/Map Viewed - when: - - c: Member/View Park Details - then: - - e: Member/Park Details Viewed - CafeDetailsViewed: - given: - - e: Member/Park Details Viewed - when: - - c: Member/View Cafe Details - then: - - e: Member/Cafe Details Viewed - TrendingSpotViewed: - given: - - e: Member/Cafe Details Viewed - when: - - c: Member/View Trending Spot - then: - - e: Member/Trending Spot Viewed - SpotReviewsViewed: - given: - - e: Member/Trending Spot Viewed - when: - - c: Member/View Spot Reviews - then: - - e: Member/Spot Reviews Viewed - ReviewAttemptGated: - given: - - e: Member/Spot Reviews Viewed - when: - - c: Member/Attempt To Leave Review - then: - - e: Member/Review Attempt Gated - SignUpInitiated: - given: - - e: Member/Review Attempt Gated - when: - - c: Member/Initiate Sign-Up - then: - - e: Member/Sign-Up Initiated - ConfirmationEmailSent: - given: - - e: Member/Sign-Up Initiated - when: - - c: Member/Send Confirmation Email - then: - - e: Member/Confirmation Email Sent - ProfileConfirmed: - given: - - e: Member/Confirmation Email Sent - when: - - c: Member/Confirm Profile - then: - - e: Member/Profile Confirmed - ReviewPosted: - given: - - e: Member/Profile Confirmed - when: - - c: Member/Post Review - then: - - e: Member/Review Posted TheUrgentSearch: steps: - t: Member/Lost Dog Listing @@ -1742,569 +1123,58 @@ slices: - c: Member/Merge Duplicate Sighting then: - e: Member/Duplicate Sighting Merged - ArrangeAPlaydate: + TheSkepticalParent: steps: - - t: Member/Propose Playdate - - c: Member/Propose Playdate - props: - proposedDogId: dog_71 - proposedTime: '2026-07-20T15:00:00Z' - location: Riverside Dog Park - - e: Member/Playdate Proposed - - t: Member/Playdate Invite - - c: Member/Accept Playdate - - e: Member/Playdate Accepted - - c: Member/Decline Playdate - - e: Member/Playdate Declined - - t: Member/Playdate Scheduled - - c: Member/Schedule Playdate - - e: Member/Playdate Scheduled - - t: Member/Cancel Playdate - - c: Member/Cancel Playdate - - e: Member/Playdate Cancelled - props: - cancelledBy: owner_of_dog_71 - reason: scheduling conflict + - t: Guest/Benefits Section + - c: Guest/View Benefits Section + - e: Guest/Benefits Section Viewed + - t: Guest/Testimonials + - c: Guest/View Testimonials + - e: Guest/Testimonials Viewed + - t: Member/Sign Up + - c: Member/Initiate Sign-Up + - e: Member/Sign-Up Initiated + - c: Member/Send Confirmation Email + - e: Member/Confirmation Email Sent + - t: Member/Confirm Profile + - c: Member/Confirm Profile + - e: Member/Profile Confirmed + - t: Member/Account Already Exists + - c: Member/Reject Sign-Up (Email Exists) + props: + attemptedEmail: parent@example.com + - e: 'Member/Sign-Up Rejected: Email Already Exists' + props: + attemptedEmail: parent@example.com + - t: Member/Log In or Reset + - c: Member/Prompt Login Or Reset + - e: Member/Login Or Reset Prompted + - t: Member/Request Password Reset + - c: Member/Request Password Reset + - e: Member/Password Reset Requested + - c: Member/Send Password Reset Email + - e: Member/Password Reset Email Sent + - t: Member/Set New Password + - c: Member/Reset Password + - e: Member/Password Reset Completed tests: - PlaydateProposed: + BenefitsSectionViewed: when: - - c: Member/Propose Playdate + - c: Guest/View Benefits Section then: - - e: Member/Playdate Proposed - PlaydateAccepted: + - e: Guest/Benefits Section Viewed + TestimonialsViewed: given: - - e: Member/Playdate Proposed + - e: Guest/Benefits Section Viewed when: - - c: Member/Accept Playdate + - c: Guest/View Testimonials then: - - e: Member/Playdate Accepted - PlaydateDeclined: + - e: Guest/Testimonials Viewed + SignUpInitiated: given: - - e: Member/Playdate Proposed + - e: Guest/Testimonials Viewed when: - - c: Member/Decline Playdate - then: - - e: Member/Playdate Declined - PlaydateScheduled: - given: - - e: Member/Playdate Accepted - when: - - c: Member/Schedule Playdate - then: - - e: Member/Playdate Scheduled - PlaydateCancelled: - given: - - e: Member/Playdate Scheduled - when: - - c: Member/Cancel Playdate - then: - - e: Member/Playdate Cancelled - MessagingDirectGroup: - steps: - - t: Member/Message Request - - c: Member/Send Message Request - - e: Member/Message Request Sent - - t: Member/Message Request Received - - c: Member/Accept Message Request - - e: Member/Message Request Accepted - - c: Member/Start Conversation - props: - conversationType: direct | group - - e: Member/Conversation Started - props: - conversationType: direct | group - - v: Member/Conversation - props: - conversationId: conv_88 - isGroup: 'false' - participantNames: Devon Price - - t: Member/Conversation - - c: Member/Send Message - - e: Member/Message Sent - - t: Member/Add Group Member - - c: Member/Add Group Member - props: - permission: any_member - - e: Member/Group Member Added - props: - permission: any_member - - c: Member/Report Message - - e: Member/Content Flagged - tests: - MessageRequestSent: - when: - - c: Member/Send Message Request - then: - - e: Member/Message Request Sent - MessageRequestAccepted: - given: - - e: Member/Message Request Sent - when: - - c: Member/Accept Message Request - then: - - e: Member/Message Request Accepted - ConversationStarted: - given: - - e: Member/Message Request Accepted - when: - - c: Member/Start Conversation - then: - - e: Member/Conversation Started - Conversation: - given: - - e: Member/Conversation Started - then: - - v: Member/Conversation - MessageSent: - given: - - e: Member/Conversation Started - when: - - c: Member/Send Message - then: - - e: Member/Message Sent - GroupMemberAdded: - given: - - e: Member/Conversation Started - when: - - c: Member/Add Group Member - then: - - e: Member/Group Member Added - UserReportsContentReportMessage: - given: - - e: Member/Message Sent - when: - - c: Member/Report Message - then: - - e: Member/Content Flagged - ActivityFeed: - steps: - - c: Member/Open Activity Feed - - e: Member/Activity Feed Page Opened - - v: Member/Activity Feed - props: - source: followed_profiles_only - - t: Member/Activity Feed - - c: Member/Like Post - - e: Member/Post Liked - - t: Member/Comment - - c: Member/Comment On Post - - e: Member/Post Commented - - c: Member/Unlike Post - - e: Member/Post Unliked - - c: Member/Report Post - - e: Member/Content Flagged - tests: - ActivityFeedPageOpened: - when: - - c: Member/Open Activity Feed - then: - - e: Member/Activity Feed Page Opened - ActivityFeed: - given: - - e: Member/Activity Feed Page Opened - then: - - v: Member/Activity Feed - PostLiked: - given: - - e: Member/Activity Feed Page Opened - when: - - c: Member/Like Post - then: - - e: Member/Post Liked - PostCommented: - given: - - e: Member/Activity Feed Page Opened - when: - - c: Member/Comment On Post - then: - - e: Member/Post Commented - PostUnliked: - given: - - e: Member/Post Liked - when: - - c: Member/Unlike Post - then: - - e: Member/Post Unliked - UserReportsContentReportPost: - given: - - e: Member/Post Commented - when: - - c: Member/Report Post - then: - - e: Member/Content Flagged - FollowAProfile: - steps: - - t: Member/View Profile - - c: Member/Follow Profile - - e: Member/Profile Followed - props: - notifiesFollowedUser: 'false' - - c: Member/Unfollow Profile - - e: Member/Profile Unfollowed - - c: Member/View Followers - - e: Member/Followers Viewed - - v: Member/Followers List - props: - userId: user_58 - name: Devon Price - followedAt: '2026-06-01' - - t: Member/Followers - - t: Member/Block Follower - - c: Member/Block Follower - - e: Member/Follower Blocked - props: - reason: harassment - tests: - ProfileFollowed: - when: - - c: Member/Follow Profile - then: - - e: Member/Profile Followed - ProfileUnfollowed: - given: - - e: Member/Profile Followed - when: - - c: Member/Unfollow Profile - then: - - e: Member/Profile Unfollowed - FollowersViewed: - given: - - e: Member/Profile Followed - when: - - c: Member/View Followers - then: - - e: Member/Followers Viewed - FollowersList: - given: - - e: Member/Followers Viewed - then: - - v: Member/Followers List - FollowerBlocked: - given: - - e: Member/Followers Viewed - when: - - c: Member/Block Follower - then: - - e: Member/Follower Blocked - # [BUILT] Profiles module. Publishing cascades DogProfileCreatedV1 to - # Discovery, which is the ONLY way a dog ever enters the swipe feed - - # Drafts are never indexed (see the new SwipingAndMatching chapter). - # DEVIATION: AddDogProfileDetails carries a `location` (latitude/ - # longitude) the yaml never listed as a prop - a disclosed gap-fill, - # since Discovery's feed needs coordinates to do distance filtering and - # nothing else in this chapter provides them. - # DEVIATION: PublishDogProfile has a SECOND guard beyond "photo - # required" - it also 409s if Location was never set (details never - # added), which the yaml's own steps didn't anticipate since it never - # modeled location as a prop in the first place. - AddDogProfile: - steps: - - t: Member/Start Dog Profile - - c: Member/Start Dog Profile - props: - maxDogProfiles: '10' - - e: Member/Dog Profile Started - - t: Member/Dog Details - - c: Member/Add Dog Profile Details - props: - name: Biscuit - breed: Labrador - ageInMonths: '36' - bio: Friendly - location: '{ latitude: 45.5, longitude: -122.6 }' - - e: Member/Dog Profile Details Added - - t: Member/Add Photo - - c: Member/Add Dog Profile Photo - props: - mediaAssetId: media_9f2a - - e: Member/Dog Profile Photo Added - - t: Member/Publish Profile - - c: Member/Publish Dog Profile - - e: Member/Dog Profile Published - - t: Member/Photo Required - - e: 'Member/Publish Blocked: Photo Required' - - t: Member/Details Required - - e: 'Member/Publish Blocked: Details Required' - props: - reason: location was never set - tests: - DogProfileStarted: - when: - - c: Member/Start Dog Profile - then: - - e: Member/Dog Profile Started - DogProfileDetailsAdded: - given: - - e: Member/Dog Profile Started - when: - - c: Member/Add Dog Profile Details - then: - - e: Member/Dog Profile Details Added - DogProfilePhotoAdded: - given: - - e: Member/Dog Profile Details Added - when: - - c: Member/Add Dog Profile Photo - then: - - e: Member/Dog Profile Photo Added - DogProfilePublished: - given: - - e: Member/Dog Profile Photo Added - when: - - c: Member/Publish Dog Profile - then: - - e: Member/Dog Profile Published - PublishBlockedPhotoRequired: - given: - - e: Member/Dog Profile Details Added - when: - - c: Member/Publish Dog Profile - then: - - e: 'Member/Publish Blocked: Photo Required' - PublishBlockedDetailsRequired: - given: - - e: Member/Dog Profile Started - when: - - c: Member/Publish Dog Profile - then: - - e: 'Member/Publish Blocked: Details Required' - RSVPingDirectly: - steps: - - t: Member/Local Events - - c: Member/View Local Events - - e: Member/Local Events Viewed - - t: Member/Event Full - - c: Member/Attempt RSVP - - e: 'Member/RSVP Blocked: Event Full' - - t: Member/RSVP Confirmed - - c: Member/Complete RSVP - - e: Member/RSVP Completed - - t: Member/Cancel RSVP - - c: Member/Cancel RSVP - - e: Member/RSVP Cancelled - - t: Member/You're On the Waitlist - - c: Member/Join Waitlist - - e: Member/Added To Waitlist - - c: Member/Notify Waitlist Of Opening - - e: Member/Waitlist Notified Of Opening - tests: - LocalEventsViewed: - when: - - c: Member/View Local Events - then: - - e: Member/Local Events Viewed - RSVPBlockedEventFull: - given: - - e: Member/Local Events Viewed - when: - - c: Member/Attempt RSVP - then: - - e: 'Member/RSVP Blocked: Event Full' - RSVPCompleted: - given: - - e: Member/Local Events Viewed - when: - - c: Member/Complete RSVP - then: - - e: Member/RSVP Completed - RSVPCancelled: - given: - - e: Member/RSVP Completed - when: - - c: Member/Cancel RSVP - then: - - e: Member/RSVP Cancelled - AddedToWaitlist: - given: - - e: 'Member/RSVP Blocked: Event Full' - when: - - c: Member/Join Waitlist - then: - - e: Member/Added To Waitlist - WaitlistNotifiedOfOpening: - given: - - e: Member/RSVP Cancelled - when: - - c: Member/Notify Waitlist Of Opening - then: - - e: Member/Waitlist Notified Of Opening - TheEventBrowser: - steps: - - c: Guest/View Local Events - - e: Guest/Local Events Viewed - - v: Guest/Local Events - props: - eventId: evt_44 - title: Bark in the Park Meetup - date: '2026-08-02' - spotsLeft: '6' - - t: Guest/Local Events - - c: Guest/Block RSVP Attempt - - e: Guest/RSVP Attempt Blocked - - t: Guest/Sign Up or Log In - - c: Guest/Prompt Sign-Up Or Log-In - - e: Guest/Sign-Up Or Log-In Prompted - - t: Guest/Log In - - c: Guest/Log In - - e: Guest/Logged In (Existing User) - - t: Member/Sign Up - - c: Member/Initiate Sign-Up - - e: Member/Sign-Up Initiated - - c: Member/Send Confirmation Email - - e: Member/Confirmation Email Sent - - t: Member/Confirm Profile - - c: Member/Confirm Profile - - e: Member/Profile Confirmed - - t: Member/Log In To Complete RSVP - - c: Member/Log In To Complete RSVP - - e: Member/Logged In To Complete RSVP - - t: Member/RSVP Confirmed - - c: Member/Complete RSVP - - e: Member/RSVP Completed - - t: Member/Event Full - - c: Member/Reject RSVP (Event Full) - - e: 'Member/RSVP Blocked: Event Full' - - t: Member/You're On the Waitlist - - c: Member/Join Waitlist - - e: Member/Added To Waitlist - tests: - LocalEventsViewed: - when: - - c: Guest/View Local Events - then: - - e: Guest/Local Events Viewed - LocalEvents: - given: - - e: Guest/Local Events Viewed - then: - - v: Guest/Local Events - RSVPAttemptBlocked: - given: - - e: Guest/Local Events Viewed - when: - - c: Guest/Block RSVP Attempt - then: - - e: Guest/RSVP Attempt Blocked - SignUpOrLogInPrompted: - given: - - e: Guest/RSVP Attempt Blocked - when: - - c: Guest/Prompt Sign-Up Or Log-In - then: - - e: Guest/Sign-Up Or Log-In Prompted - LoggedInExistingUser: - given: - - e: Guest/Sign-Up Or Log-In Prompted - when: - - c: Guest/Log In - then: - - e: Guest/Logged In (Existing User) - SignUpInitiated: - given: - - e: Guest/Sign-Up Or Log-In Prompted - when: - - c: Member/Initiate Sign-Up - then: - - e: Member/Sign-Up Initiated - ConfirmationEmailSent: - given: - - e: Member/Sign-Up Initiated - when: - - c: Member/Send Confirmation Email - then: - - e: Member/Confirmation Email Sent - ProfileConfirmed: - given: - - e: Member/Confirmation Email Sent - when: - - c: Member/Confirm Profile - then: - - e: Member/Profile Confirmed - LoggedIn: - given: - - e: Member/Profile Confirmed - when: - - c: Member/Log In To Complete RSVP - then: - - e: Member/Logged In To Complete RSVP - RSVPCompletedAfterSignUp: - given: - - e: Member/Logged In To Complete RSVP - when: - - c: Member/Complete RSVP - then: - - e: Member/RSVP Completed - RSVPCompletedAfterExistingUserLogin: - given: - - e: Guest/Logged In (Existing User) - when: - - c: Member/Complete RSVP - then: - - e: Member/RSVP Completed - RSVPBlockedEventFull: - when: - - c: Member/Reject RSVP (Event Full) - then: - - e: 'Member/RSVP Blocked: Event Full' - AddedToWaitlist: - given: - - e: 'Member/RSVP Blocked: Event Full' - when: - - c: Member/Join Waitlist - then: - - e: Member/Added To Waitlist - TheSkepticalParent: - steps: - - t: Guest/Benefits Section - - c: Guest/View Benefits Section - - e: Guest/Benefits Section Viewed - - t: Guest/Testimonials - - c: Guest/View Testimonials - - e: Guest/Testimonials Viewed - - t: Member/Sign Up - - c: Member/Initiate Sign-Up - - e: Member/Sign-Up Initiated - - c: Member/Send Confirmation Email - - e: Member/Confirmation Email Sent - - t: Member/Confirm Profile - - c: Member/Confirm Profile - - e: Member/Profile Confirmed - - t: Member/Account Already Exists - - c: Member/Reject Sign-Up (Email Exists) - props: - attemptedEmail: parent@example.com - - e: 'Member/Sign-Up Rejected: Email Already Exists' - props: - attemptedEmail: parent@example.com - - t: Member/Log In or Reset - - c: Member/Prompt Login Or Reset - - e: Member/Login Or Reset Prompted - - t: Member/Request Password Reset - - c: Member/Request Password Reset - - e: Member/Password Reset Requested - - c: Member/Send Password Reset Email - - e: Member/Password Reset Email Sent - - t: Member/Set New Password - - c: Member/Reset Password - - e: Member/Password Reset Completed - tests: - BenefitsSectionViewed: - when: - - c: Guest/View Benefits Section - then: - - e: Guest/Benefits Section Viewed - TestimonialsViewed: - given: - - e: Guest/Benefits Section Viewed - when: - - c: Guest/View Testimonials - then: - - e: Guest/Testimonials Viewed - SignUpInitiated: - given: - - e: Guest/Testimonials Viewed - when: - - c: Member/Initiate Sign-Up + - c: Member/Initiate Sign-Up then: - e: Member/Sign-Up Initiated ConfirmationEmailSent: @@ -2325,395 +1195,73 @@ slices: given: - e: Guest/Testimonials Viewed when: - - c: Member/Reject Sign-Up (Email Exists) - then: - - e: 'Member/Sign-Up Rejected: Email Already Exists' - LoginOrResetPrompted: - given: - - e: 'Member/Sign-Up Rejected: Email Already Exists' - when: - - c: Member/Prompt Login Or Reset - then: - - e: Member/Login Or Reset Prompted - PasswordResetRequested: - given: - - e: Member/Login Or Reset Prompted - when: - - c: Member/Request Password Reset - then: - - e: Member/Password Reset Requested - PasswordResetEmailSent: - given: - - e: Member/Password Reset Requested - when: - - c: Member/Send Password Reset Email - then: - - e: Member/Password Reset Email Sent - PasswordResetCompleted: - given: - - e: Member/Password Reset Email Sent - when: - - c: Member/Reset Password - then: - - e: Member/Password Reset Completed - TrainingAndTips: - steps: - - c: Member/Open Training Tips - - e: Member/Training Tips Page Opened - - v: Member/Training Tips - props: - tailoredToDogId: dog_71 - tailoredByBreed: 'true' - tailoredByAge: 'true' - - t: Member/Training Tips - - c: Member/Save Tip - props: - savedToCollection: Favourites - itemCategory: training_tip - - e: Member/Tip Saved - props: - savedToCollection: Favourites - itemCategory: training_tip - tests: - TrainingTipsPageOpened: - when: - - c: Member/Open Training Tips - then: - - e: Member/Training Tips Page Opened - TrainingTips: - given: - - e: Member/Training Tips Page Opened - then: - - v: Member/Training Tips - TipSaved: - given: - - e: Member/Training Tips Page Opened - when: - - c: Member/Save Tip - then: - - e: Member/Tip Saved - ManagingSavedDogsSpots: - steps: - - c: Member/View Saved Items - - e: Member/Saved Items Viewed - - v: Member/Favourites - props: - itemId: dog_71 - itemType: shelter_dog | dog_to_dog | spot | tip - savedAt: '2026-07-10' - - t: Member/My Favourites - - c: Member/Remove Saved Item - - e: Member/Saved Item Removed - - t: Member/Pursue Match - - c: Member/Pursue Saved Match - props: - matchType: shelter_dog | dog_to_dog - - e: Member/Saved Match Pursued - props: - matchType: shelter_dog | dog_to_dog - - c: Member/Re-add Saved Item - - e: Member/Saved Item Re-added - props: - history: added -> removed -> re-added - - c: Member/Start Application From Saved Match - - e: Member/Application Started From Saved Match - props: - matchType: shelter_dog - - t: Member/It's a Match! - - c: Member/Confirm Dog-to-Dog Match - - e: Member/Messaging Unlocked (Dog Match) - props: - matchType: dog_to_dog - capabilities: direct_message, video_chat - - t: Member/Dog No Longer Available - - c: Member/Block Application Start (Dog No Longer Available) - - e: 'Member/Application Start Blocked: Dog No Longer Available' - tests: - SavedItemsViewed: - when: - - c: Member/View Saved Items - then: - - e: Member/Saved Items Viewed - Favourites: - given: - - e: Member/Saved Items Viewed - then: - - v: Member/Favourites - SavedItemRemoved: - given: - - e: Member/Saved Items Viewed - when: - - c: Member/Remove Saved Item - then: - - e: Member/Saved Item Removed - SavedMatchPursued: - given: - - e: Member/Saved Items Viewed - when: - - c: Member/Pursue Saved Match - then: - - e: Member/Saved Match Pursued - SavedItemReAdded: - given: - - e: Member/Saved Item Removed - when: - - c: Member/Re-add Saved Item - then: - - e: Member/Saved Item Re-added - ApplicationStartedFromSavedMatch: - given: - - e: Member/Saved Match Pursued - when: - - c: Member/Start Application From Saved Match - then: - - e: Member/Application Started From Saved Match - MessagingUnlockedDogMatch: - given: - - e: Member/Saved Match Pursued - when: - - c: Member/Confirm Dog-to-Dog Match - then: - - e: Member/Messaging Unlocked (Dog Match) - ApplicationStartBlockedDogNoLongerAvailable: - given: - - e: Member/Saved Match Pursued - when: - - c: Member/Block Application Start (Dog No Longer Available) - then: - - e: 'Member/Application Start Blocked: Dog No Longer Available' - # [BUILT] Places module. DEVIATION: v1's ClaimABusinessListing chapter - # is entirely about claiming an "existing listing" - it never shows how - # one first comes into existence. "Create Place Listing" is a disclosed - # gap-fill added here (no v1 counterpart at all) so reviews have - # something to attach to; the creating caller becomes the Place's - # owner directly, NOT wired through ClaimABusinessListing's real - # request/verify/admin-override/ownership-transfer workflow (still - # entirely unbuilt - see that chapter, unchanged, elsewhere in this - # file). "Respond To Review"'s `responderRole` gate checks - # Place.OwnerId directly for the same reason - no verified - # business-owner concept exists yet. - LeaveAReviewRestaurantOrDogPark: - steps: - - t: Place Owner/Create Place Listing - - c: Place Owner/Create Place Listing - props: - name: Bark Park - placeType: Restaurant | DogPark | Groomer | Trainer | Walker | Minder | Breeder - - e: Place Owner/Place Listing Created - - t: Member/Write Review - - c: Member/Write Review - props: - rating: '5' - visitVerificationRequired: 'false' - - e: Member/Review Written - props: - placeId: place_44 - rating: '5' - - c: Member/Publish Review - - e: Member/Review Published - - t: Member/Edit Review - - c: Member/Edit Review - - e: Member/Review Edited - - t: Member/Remove Review - - c: Member/Remove Review - - e: Member/Review Removed - - t: Member/Respond to Review - - c: Place Owner/Respond To Review - - e: Member/Review Response Posted - props: - responderRole: BusinessOwner | ParkOwner | DogWalker | DogTrainer - - c: Member/Report Review - - e: Member/Content Flagged - tests: - PlaceListingCreated: - when: - - c: Place Owner/Create Place Listing - then: - - e: Place Owner/Place Listing Created - ReviewWritten: - given: - - e: Place Owner/Place Listing Created - when: - - c: Member/Write Review - then: - - e: Member/Review Written - ReviewPublished: - given: - - e: Member/Review Written - when: - - c: Member/Publish Review - then: - - e: Member/Review Published - ReviewEdited: - given: - - e: Member/Review Published - when: - - c: Member/Edit Review - then: - - e: Member/Review Edited - ReviewRemoved: - given: - - e: Member/Review Published - when: - - c: Member/Remove Review - then: - - e: Member/Review Removed - ReviewResponsePosted: - given: - - e: Member/Review Published - when: - - c: Place Owner/Respond To Review - then: - - e: Member/Review Response Posted - UserReportsContentReportReview: - given: - - e: Member/Review Published - when: - - c: Member/Report Review - then: - - e: Member/Content Flagged - TheGiftShopper: - steps: - - c: Guest/View Merch Store - - e: Guest/Merch Store Viewed - - v: Guest/Merch Products - props: - productId: prod_19 - name: K9Crush Adopt Don't Shop Tee - price: '24.00' - - t: Guest/Merch Store - - t: Guest/Product Details - - c: Guest/View Product - - e: Guest/Product Viewed - - c: Guest/Add Item To Cart - props: - productId: prod_123 - quantity: '1' - - e: Guest/Item Added To Cart - props: - productId: prod_123 - quantity: '1' - - c: Guest/Gate Checkout - - e: Guest/Checkout Gated - - t: Guest/Create an Account? - - c: Guest/Show Sign-Up Incentive - - e: Guest/Sign-Up Incentive Shown - - t: Guest/Guest Checkout - - c: Guest/Create Guest Account - props: - accountType: guest - convertibleToFullAccount: 'true' - - e: Guest/Guest Account Created - props: - accountType: guest - convertibleToFullAccount: 'true' - - c: Guest/Expire Cart - - e: Guest/Cart Expired - - t: Guest/Payment - - c: Guest/Submit Payment - props: - amount: '49.99' - - e: Guest/Payment Submitted - props: - amount: '49.99' - - t: Guest/Payment Failed - - c: Guest/Decline Payment - - e: Guest/Payment Failed - - c: Guest/Prompt Payment Retry - - e: Guest/Payment Retry Prompted - - c: Guest/Confirm Payment - - e: Guest/Payment Succeeded - - t: Guest/Order Confirmed - - c: Guest/Place Order - - e: Guest/Order Placed - tests: - MerchStoreViewed: - when: - - c: Guest/View Merch Store - then: - - e: Guest/Merch Store Viewed - MerchProducts: - given: - - e: Guest/Merch Store Viewed - then: - - v: Guest/Merch Products - ProductViewed: - given: - - e: Guest/Merch Store Viewed - when: - - c: Guest/View Product - then: - - e: Guest/Product Viewed - ItemAddedToCart: - given: - - e: Guest/Product Viewed - when: - - c: Guest/Add Item To Cart - then: - - e: Guest/Item Added To Cart - CheckoutGated: - given: - - e: Guest/Item Added To Cart - when: - - c: Guest/Gate Checkout - then: - - e: Guest/Checkout Gated - SignUpIncentiveShown: - given: - - e: Guest/Checkout Gated - when: - - c: Guest/Show Sign-Up Incentive - then: - - e: Guest/Sign-Up Incentive Shown - GuestAccountCreated: - given: - - e: Guest/Sign-Up Incentive Shown - when: - - c: Guest/Create Guest Account + - c: Member/Reject Sign-Up (Email Exists) then: - - e: Guest/Guest Account Created - CartExpired: + - e: 'Member/Sign-Up Rejected: Email Already Exists' + LoginOrResetPrompted: given: - - e: Guest/Guest Account Created + - e: 'Member/Sign-Up Rejected: Email Already Exists' when: - - c: Guest/Expire Cart + - c: Member/Prompt Login Or Reset then: - - e: Guest/Cart Expired - PaymentSubmitted: + - e: Member/Login Or Reset Prompted + PasswordResetRequested: given: - - e: Guest/Guest Account Created + - e: Member/Login Or Reset Prompted when: - - c: Guest/Submit Payment + - c: Member/Request Password Reset then: - - e: Guest/Payment Submitted - PaymentFailed: + - e: Member/Password Reset Requested + PasswordResetEmailSent: given: - - e: Guest/Payment Submitted + - e: Member/Password Reset Requested when: - - c: Guest/Decline Payment + - c: Member/Send Password Reset Email then: - - e: Guest/Payment Failed - PaymentRetryPrompted: + - e: Member/Password Reset Email Sent + PasswordResetCompleted: given: - - e: Guest/Payment Failed + - e: Member/Password Reset Email Sent when: - - c: Guest/Prompt Payment Retry + - c: Member/Reset Password then: - - e: Guest/Payment Retry Prompted - PaymentSucceeded: - given: - - e: Guest/Payment Retry Prompted + - e: Member/Password Reset Completed + TrainingAndTips: + steps: + - c: Member/Open Training Tips + - e: Member/Training Tips Page Opened + - v: Member/Training Tips + props: + tailoredToDogId: dog_71 + tailoredByBreed: 'true' + tailoredByAge: 'true' + - t: Member/Training Tips + - c: Member/Save Tip + props: + savedToCollection: Favourites + itemCategory: training_tip + - e: Member/Tip Saved + props: + savedToCollection: Favourites + itemCategory: training_tip + tests: + TrainingTipsPageOpened: when: - - c: Guest/Confirm Payment + - c: Member/Open Training Tips + then: + - e: Member/Training Tips Page Opened + TrainingTips: + given: + - e: Member/Training Tips Page Opened then: - - e: Guest/Payment Succeeded - OrderPlaced: + - v: Member/Training Tips + TipSaved: given: - - e: Guest/Payment Succeeded + - e: Member/Training Tips Page Opened when: - - c: Guest/Place Order + - c: Member/Save Tip then: - - e: Guest/Order Placed + - e: Member/Tip Saved # [PARTIAL] Identity module (deletion saga uses ADR-026 Wolverine # scheduled messages for the 30-day grace period) + ShelterAdoption's # WithdrawApplicationsOnAccountDeletionRequested (cross-module reaction @@ -2850,166 +1398,6 @@ slices: - c: Member/Update Notification Preferences then: - e: Member/Notification Preferences Updated - # ============================================================ - # New chapters below - real, load-bearing slices built this session - # that had no counterpart anywhere on the original 35-chapter board. - # ============================================================ - # [BUILT, NEW - no v1 counterpart] Discovery module, event-sourced (one - # Marten stream per unordered dog pair, MatchStream.IdFor(dogId1,dogId2) - # - a deterministic hash of the sorted pair, so either dog swiping - # first lands on the same stream). None of the 35 original board - # chapters actually modeled the swipe mechanic itself, only its - # downstream outcomes (TheWindowShopper's guest-side flows, various - # chapters' "It's a Match!"/messaging-unlocked framing) - this chapter - # fills that gap directly from the real implementation. Ownership-gated - # (SwiperDogId must belong to the caller, verified against Discovery's - # own DiscoveryFeedItem read-model, never a cross-module Domain - # reference). UndoLastSwipe never touches match detection - it only - # ever reverses the swipe itself, even if a match had already formed. - # DetectMutualMatch is a same-module automation (Marten event - # forwarding, not scheduled/HTTP-triggered) reacting to DogLiked - - # cascades MatchCreatedV1 only on a genuinely NEW mutual match - # (idempotency via live-aggregated state, not a persisted flag). - SwipingAndMatching: - steps: - - t: Member/Discovery Feed - - c: Member/Swipe On Dog - props: - swiperDogId: dog_12 - targetDogId: dog_71 - liked: 'true' - - e: Member/Dog Liked - - c: Member/Swipe On Dog - props: - liked: 'false' - - e: Member/Dog Passed - - t: Member/Undo Swipe - - c: Member/Undo Last Swipe - - e: Member/Swipe Undone - - t: Member/No Swipe To Undo - - e: 'Member/Undo Blocked: No Swipe To Undo' - - c: Member/Detect Mutual Match - - e: Member/Match Formed - props: - dogAId: dog_12 - dogBId: dog_71 - - e: Member/Match Created - props: - cascadedTo: Chat (CreateConversationOnMatch), Notifications (NotifyOnMatch) - tests: - DogLiked: - when: - - c: Member/Swipe On Dog - then: - - e: Member/Dog Liked - DogPassed: - when: - - c: Member/Swipe On Dog - then: - - e: Member/Dog Passed - SwipeUndone: - given: - - e: Member/Dog Liked - when: - - c: Member/Undo Last Swipe - then: - - e: Member/Swipe Undone - UndoBlockedNoSwipeToUndo: - when: - - c: Member/Undo Last Swipe - then: - - e: 'Member/Undo Blocked: No Swipe To Undo' - MatchFormedAndCreated: - given: - - e: Member/Dog Liked - when: - - c: Member/Detect Mutual Match - then: - - e: Member/Match Formed - - e: Member/Match Created - # [PARTIAL, NEW - no v1 counterpart] Chat module, event-sourced. Built - # as REST-only + match-triggered, a deliberately narrower design than - # v1's own MessagingDirectGroup chapter (kept unchanged, elsewhere in - # this file) - see that chapter for the request/accept/group-chat - # design that was NOT taken. A conversation is created automatically - # (CreateConversationOnMatch, a cross-module automation reacting to - # Discovery's MatchCreatedV1) the moment two - # dogs mutually match - there is no message-request/accept step at all, - # and the conversation's own stream id directly reuses Discovery's - # MatchId (no second pair-hash derivation - MatchCreatedV1 already - # provides a stable unique key for the pair). "Get My Conversations" was - # a necessary addition beyond the docs' four named slices - without it - # a caller has no way to discover a conversationId at all once a match - # creates one asynchronously. No real-time push (SignalR) - a caller - # polls GetMyConversations/GetConversationHistory. No group chat, no - # manual "message anyone" flow, no Report Message -> Content Flagged - # (Chat is one of the two still-missing "Content Flagged" producers, - # alongside ActivityFeed - see ModeratingFlaggedContentUserReports' - # note on this). - ChatMatchTriggeredMessaging: - steps: - - e: Member/Match Created - - c: Member/Create Conversation On Match - - e: Member/Conversation Created - props: - conversationId: same as matchId - - t: Member/My Conversations - - v: Member/My Conversations - props: - conversationId: conv_88 - otherOwnerId: owner_44 - - t: Member/Conversation - - v: Member/Conversation History - props: - conversationId: conv_88 - messages: '[{ senderOwnerId, text, sentAt }]' - - c: Member/Send Message - props: - text: Hi! Would love to set up a playdate. - - e: Member/Message Sent - - c: Member/Mark As Read - props: - lastReadMessageId: msg_501 - - e: Member/Message Read - tests: - ConversationCreatedOnMatch: - given: - - e: Member/Match Created - when: - - c: Member/Create Conversation On Match - then: - - e: Member/Conversation Created - ConversationCreationIsIdempotent: - given: - - e: Member/Conversation Created - when: - - c: Member/Create Conversation On Match - then: - - e: Member/Conversation Created - MyConversations: - given: - - e: Member/Conversation Created - then: - - v: Member/My Conversations - ConversationHistory: - given: - - e: Member/Conversation Created - then: - - v: Member/Conversation History - MessageSent: - given: - - e: Member/Conversation Created - when: - - c: Member/Send Message - then: - - e: Member/Message Sent - MessageRead: - given: - - e: Member/Message Sent - when: - - c: Member/Mark As Read - then: - - e: Member/Message Read # [BUILT, NEW - no v1 counterpart] Identity module. Covers three # foundational Identity slices none of the 35 original chapters ever # modeled, since they're platform plumbing rather than a member/shelter @@ -3188,6 +1576,201 @@ slices: - c: Admin/Decline Dog Surrender then: - e: Admin/Dog Surrender Declined + # v3 ENRICHMENT: [PLANNED] "FullIntake" mode - a per-shelter alternative to + # the [BUILT] flow above, discovered by comparing this chapter against the + # eventmodelers board (1d0fbac8...), which already modeled a much more + # elaborate real-world intake pipeline than this yaml's original + # Request/Review/Accept-or-Decline shape. Rather than replace the [BUILT] + # flow (working, tested, committed), this is additive: ShelterAccount + # gains SurrenderIntakeMode (Simple | FullIntake, Simple = 0 default - + # same safe-default convention as DogListing.Status.Available = 0, no + # migration needed for existing shelters). Simple-mode shelters keep + # today's behavior byte-for-byte: Request -> Review -> (optional + # Additional Details loop) -> Accept (cascades straight into + # ShelterManagingListings' Add Dog Listing) or Decline, nothing below + # this comment applies to them. FullIntake-mode shelters take the exact + # same path through Accept (the DogListing is still created immediately, + # NotReadyYet, the same way) but "Dog Surrender Accepted" now also opens + # an in-progress intake record instead of closing the request, and the + # 9 Shelter Staff steps below become available. "List Dog For Adoption" + # is deliberately NOT a new command - it's ShelterManagingListings' + # existing "Update Listing Status" (-> Available), reused rather than + # duplicated, same restraint as FosteringADog's "Convert Foster To + # Adoption" reusing SubmitApplicationHandler. "Record Euthanasia + # Outcome" cascades into ShelterManagingListings' existing "Remove Dog + # Listing" (reason: euthanized) for the same reason - no new terminal + # DogListingStatus value needed. A minimal "Configure Surrender Intake + # Mode" step is included so the field is actually settable; a broader + # per-shelter preferences/settings chapter (the user also floated staff + # language preference) is explicitly out of scope here - deliberately + # not generalized until a real second use case exists. + SurrenderingYourDogFullIntake: + steps: + - t: Shelter Staff/Shelter Settings + - c: Shelter Staff/Configure Surrender Intake Mode + props: + mode: Simple | FullIntake + - e: Shelter Staff/Surrender Intake Mode Configured + props: + mode: Simple | FullIntake + - t: Shelter Staff/Surrender Intake Pipeline + - c: Shelter Staff/Add To Waiting List + props: + surrenderRequestId: srr_118 + - e: Shelter Staff/Added To Waiting List + props: + waitlistPosition: '4' + - c: Shelter Staff/Schedule Intake Appointment + props: + surrenderRequestId: srr_118 + appointmentDate: '2026-08-05' + - e: Shelter Staff/Intake Appointment Scheduled + props: + appointmentDate: '2026-08-05' + - c: Shelter Staff/Complete Surrender Paperwork + props: + surrenderRequestId: srr_118 + legalTransferSigned: 'true' + ownershipProofType: dog_licence + - e: Shelter Staff/Surrender Paperwork Completed + props: + legalTransferSigned: 'true' + ownershipProofType: dog_licence + - c: Shelter Staff/Pay Surrender Fee + props: + surrenderRequestId: srr_118 + feeAmount: '150' + feeWaived: 'false' + - e: Shelter Staff/Surrender Fee Paid + props: + feeAmount: '150' + feeWaived: 'false' + - c: Shelter Staff/Perform Health Check + props: + surrenderRequestId: srr_118 + dogId: dog_71 + performedBy: staff_213 + - e: Shelter Staff/Health Check Completed + props: + vaccinesUpdated: 'true' + microchipStatus: registered + healthIssuesFound: 'false' + - c: Shelter Staff/Perform Behavior Test + props: + surrenderRequestId: srr_118 + dogId: dog_71 + performedBy: staff_213 + - e: Shelter Staff/Behavior Test Completed + props: + suitableForRehoming: 'true' + behaviorNotes: Friendly, no aggression observed + - c: Shelter Staff/Finalize Ownership Transfer + props: + surrenderRequestId: srr_118 + dogId: dog_71 + shelterId: shelter_44 + - e: Shelter Staff/Ownership Transferred + props: + previousOwnerId: user_812 + transferredAt: '2026-07-15T10:00:00Z' + - t: Shelter Staff/List Dog For Adoption + - c: Shelter Staff/Update Listing Status + props: + status: Available + cascadedFrom: SurrenderingYourDogFullIntake (Finalize Ownership Transfer) + - e: Shelter Staff/Listing Status Updated + - t: Shelter Staff/Flag For Outcome Review + - c: Shelter Staff/Flag For Outcome Review + props: + dogId: dog_71 + reason: Failed behavior test, not suitable for rehoming + flaggedBy: staff_213 + - e: Shelter Staff/Outcome Review Flagged + - t: Shelter Staff/Record Euthanasia Outcome + - c: Shelter Staff/Record Euthanasia Outcome + props: + dogId: dog_71 + reason: Untreatable illness, no safe placement available + authorizedBy: veterinarian + - e: Shelter Staff/Euthanasia Recorded + props: + cascadedTo: ShelterManagingListings (Remove Dog Listing, reason euthanized) + tests: + SurrenderIntakeModeConfigured: + when: + - c: Shelter Staff/Configure Surrender Intake Mode + then: + - e: Shelter Staff/Surrender Intake Mode Configured + AddedToWaitingList: + given: + - e: Admin/Dog Surrender Accepted + when: + - c: Shelter Staff/Add To Waiting List + then: + - e: Shelter Staff/Added To Waiting List + IntakeAppointmentScheduled: + given: + - e: Admin/Dog Surrender Accepted + when: + - c: Shelter Staff/Schedule Intake Appointment + then: + - e: Shelter Staff/Intake Appointment Scheduled + SurrenderPaperworkCompleted: + given: + - e: Admin/Dog Surrender Accepted + when: + - c: Shelter Staff/Complete Surrender Paperwork + then: + - e: Shelter Staff/Surrender Paperwork Completed + SurrenderFeePaid: + given: + - e: Admin/Dog Surrender Accepted + when: + - c: Shelter Staff/Pay Surrender Fee + then: + - e: Shelter Staff/Surrender Fee Paid + HealthCheckCompleted: + given: + - e: Admin/Dog Surrender Accepted + when: + - c: Shelter Staff/Perform Health Check + then: + - e: Shelter Staff/Health Check Completed + BehaviorTestCompleted: + given: + - e: Admin/Dog Surrender Accepted + when: + - c: Shelter Staff/Perform Behavior Test + then: + - e: Shelter Staff/Behavior Test Completed + OwnershipTransferred: + given: + - e: Shelter Staff/Behavior Test Completed + when: + - c: Shelter Staff/Finalize Ownership Transfer + then: + - e: Shelter Staff/Ownership Transferred + DogListedForAdoptionAfterIntake: + given: + - e: Shelter Staff/Ownership Transferred + when: + - c: Shelter Staff/Update Listing Status + then: + - e: Shelter Staff/Listing Status Updated + OutcomeReviewFlagged: + given: + - e: Shelter Staff/Behavior Test Completed + when: + - c: Shelter Staff/Flag For Outcome Review + then: + - e: Shelter Staff/Outcome Review Flagged + EuthanasiaRecorded: + given: + - e: Shelter Staff/Outcome Review Flagged + when: + - c: Shelter Staff/Record Euthanasia Outcome + then: + - e: Shelter Staff/Euthanasia Recorded # [BUILT] ShelterAdoption module extension (2026-07-22). A member (the # "Foster Caregiver" swimlane below) providing temporary care for a # shelter's dog. Foster placement is modeled as a status change on an @@ -3427,3 +2010,444 @@ slices: - c: Volunteer/Submit Home Check Report then: - e: Volunteer/Home Check Report Submitted + # ============================================================ + # Board-only additions below (added to the eventmodelers board + # 2026-07-30, never before in this yaml). All [PLANNED], no code, no + # field specs decided on the board itself - props intentionally omitted + # below rather than invented, unlike this file's other [PLANNED] + # chapters. See memory (scheduling-module-scope, donations-module-scope, + # gdpr-sar-requirement) for the full design reasoning. + # ============================================================ + # [PLANNED] New Scheduling module (chapter c28df10c-...). A single + # generic, purpose-tagged Appointment concept (HomeCheck | FosterHandover + # | SurrenderIntake, linking back to the source entity id) reused across + # three automation entry points rather than three separate booking flows. + # "System/Propose ... Appointment" steps below have no human actor on the + # board (an AUTOMATION node, not a SCREEN) - each is triggered by an + # existing event in another chapter, noted via `triggeredBy`. Actor + # attribution for the shared downstream lifecycle (Confirm/Reschedule/ + # Complete/Cancel/No-Show) is a judgment call made while writing this + # into the yaml (Shelter Staff runs the lifecycle, the counterparty can + # request a reschedule) - not yet confirmed on the board itself. + BookingAppointments: + steps: + - c: System/Propose Home Check Appointment + props: + triggeredBy: VolunteeringAndHomeChecks (Accept Home Check Assignment) + - e: System/Home Check Appointment Proposed + - c: System/Propose Foster Handover Appointment + props: + triggeredBy: FosteringADog (Place Dog In Foster) + - e: System/Foster Handover Appointment Proposed + - c: System/Propose Surrender Intake Appointment + props: + triggeredBy: SurrenderingYourDogFullIntake (Review Surrender Request) + - e: System/Surrender Intake Appointment Proposed + - v: Shelter Staff/Appointments Queue + - v: Member/My Appointments + - t: Shelter Staff/Confirm Appointment + - c: Shelter Staff/Confirm Appointment + - e: Shelter Staff/Appointment Confirmed + - t: Member/Request Reschedule + - c: Member/Request Reschedule + - e: Member/Appointment Reschedule Requested + - t: Shelter Staff/Reschedule Appointment + - c: Shelter Staff/Reschedule Appointment + - e: Shelter Staff/Appointment Rescheduled + - t: Shelter Staff/Complete Appointment + - c: Shelter Staff/Complete Appointment + - e: Shelter Staff/Appointment Completed + - t: Shelter Staff/Cancel Appointment + - c: Shelter Staff/Cancel Appointment + - e: Shelter Staff/Appointment Cancelled + - t: Shelter Staff/Record Appointment No-Show + - c: Shelter Staff/Record Appointment No-Show + - e: Shelter Staff/Appointment No-Show Recorded + tests: + HomeCheckAppointmentProposed: + when: + - c: System/Propose Home Check Appointment + then: + - e: System/Home Check Appointment Proposed + FosterHandoverAppointmentProposed: + when: + - c: System/Propose Foster Handover Appointment + then: + - e: System/Foster Handover Appointment Proposed + SurrenderIntakeAppointmentProposed: + when: + - c: System/Propose Surrender Intake Appointment + then: + - e: System/Surrender Intake Appointment Proposed + AppointmentConfirmed: + given: + - e: System/Home Check Appointment Proposed + when: + - c: Shelter Staff/Confirm Appointment + then: + - e: Shelter Staff/Appointment Confirmed + AppointmentRescheduleRequested: + given: + - e: Shelter Staff/Appointment Confirmed + when: + - c: Member/Request Reschedule + then: + - e: Member/Appointment Reschedule Requested + AppointmentRescheduled: + given: + - e: Member/Appointment Reschedule Requested + when: + - c: Shelter Staff/Reschedule Appointment + then: + - e: Shelter Staff/Appointment Rescheduled + AppointmentCompleted: + given: + - e: Shelter Staff/Appointment Confirmed + when: + - c: Shelter Staff/Complete Appointment + then: + - e: Shelter Staff/Appointment Completed + AppointmentCancelled: + given: + - e: Shelter Staff/Appointment Confirmed + when: + - c: Shelter Staff/Cancel Appointment + then: + - e: Shelter Staff/Appointment Cancelled + AppointmentNoShowRecorded: + given: + - e: Shelter Staff/Appointment Confirmed + when: + - c: Shelter Staff/Record Appointment No-Show + then: + - e: Shelter Staff/Appointment No-Show Recorded + # [PLANNED] Scheduling module, chapter B (252fff51-...). Deliberately kept + # simple - no recurrence-template/auto-generation engine, just individual + # shift instances Shelter Staff create directly. A recurrence engine was + # considered and explicitly deferred, not an oversight. + VolunteerShiftRoster: + steps: + - t: Shelter Staff/Create Shift + - c: Shelter Staff/Create Shift + - e: Shelter Staff/Shift Created + - v: Volunteer/Open Shifts + - t: Volunteer/Sign Up For Shift + - c: Volunteer/Sign Up For Shift + - e: Volunteer/Volunteer Signed Up For Shift + - v: Shelter Staff/Shift Roster + - t: Volunteer/Cancel Shift Signup + - c: Volunteer/Cancel Shift Signup + - e: Volunteer/Shift Signup Cancelled + - t: Shelter Staff/Cancel Shift + - c: Shelter Staff/Cancel Shift + - e: Shelter Staff/Shift Cancelled + tests: + ShiftCreated: + when: + - c: Shelter Staff/Create Shift + then: + - e: Shelter Staff/Shift Created + OpenShifts: + given: + - e: Shelter Staff/Shift Created + then: + - v: Volunteer/Open Shifts + VolunteerSignedUpForShift: + given: + - e: Shelter Staff/Shift Created + when: + - c: Volunteer/Sign Up For Shift + then: + - e: Volunteer/Volunteer Signed Up For Shift + ShiftRoster: + given: + - e: Volunteer/Volunteer Signed Up For Shift + then: + - v: Shelter Staff/Shift Roster + ShiftSignupCancelled: + given: + - e: Volunteer/Volunteer Signed Up For Shift + when: + - c: Volunteer/Cancel Shift Signup + then: + - e: Volunteer/Shift Signup Cancelled + ShiftCancelled: + given: + - e: Shelter Staff/Shift Created + when: + - c: Shelter Staff/Cancel Shift + then: + - e: Shelter Staff/Shift Cancelled + # [PLANNED] New Donations module, chapter A (4bbee2ff-...). A shared + # payment shape (Start Donation -> gateway processes it -> Payment + # Succeeded/Payment Failed -> Donation Completed) with a recurring + # branch on top. Guest checkout allowed (AskUserQuestion decision, + # 2026-07-30) - no forced OwnerAccount signup, matching every reference + # shelter site reviewed. "Complete Donation" and "Start Recurring + # Subscription" are automations (System actor, no SCREEN on the board) + # triggered by Payment Succeeded, not user-issued commands. + MakingADonation: + steps: + - t: Guest/Make A Donation + - c: Guest/Start Donation + props: + isRecurring: 'true' + - e: Guest/Donation Started + - c: System/Charge Payment + - e: System/Payment Succeeded + - c: System/Report Payment Failure + - e: System/Payment Failed + - c: System/Complete Donation + props: + triggeredBy: Payment Succeeded + - e: System/Donation Completed + - c: System/Start Recurring Subscription + props: + triggeredBy: Payment Succeeded, isRecurring true + - e: System/Recurring Donation Subscription Started + - v: Member/My Donations + - v: Shelter Staff/Donations Received + tests: + DonationStarted: + when: + - c: Guest/Start Donation + then: + - e: Guest/Donation Started + PaymentSucceeded: + given: + - e: Guest/Donation Started + when: + - c: System/Charge Payment + then: + - e: System/Payment Succeeded + PaymentFailed: + given: + - e: Guest/Donation Started + when: + - c: System/Report Payment Failure + then: + - e: System/Payment Failed + DonationCompleted: + given: + - e: System/Payment Succeeded + when: + - c: System/Complete Donation + then: + - e: System/Donation Completed + RecurringDonationSubscriptionStarted: + given: + - e: System/Payment Succeeded + when: + - c: System/Start Recurring Subscription + then: + - e: System/Recurring Donation Subscription Started + # [PLANNED] Donations module, chapter B (41afcec8-...). Reuses the same + # Payment Succeeded/Payment Failed event names as MakingADonation's + # cross-chapter trigger labels (same convention as BookingAppointments' + # triggeredBy notes). Charge Recurring Donation's success feeds back into + # MakingADonation's Complete Donation automation - not duplicated here. + ManagingARecurringDonation: + steps: + - c: System/Charge Recurring Donation + props: + triggeredBy: Recurring Charge Due + - e: System/Recurring Donation Charged + - c: System/Report Recurring Payment Failure + props: + triggeredBy: Recurring Charge Due + - e: System/Recurring Donation Charge Failed + - c: System/Flag Recurring Payment Failed + props: + triggeredBy: Payment Failed + - e: System/Recurring Payment Failed + - t: Member/Cancel Recurring Donation + - c: Member/Cancel Recurring Donation + - e: Member/Recurring Donation Cancelled + tests: + RecurringDonationCharged: + when: + - c: System/Charge Recurring Donation + then: + - e: System/Recurring Donation Charged + RecurringDonationChargeFailed: + when: + - c: System/Report Recurring Payment Failure + then: + - e: System/Recurring Donation Charge Failed + RecurringPaymentFlaggedFailed: + given: + - e: System/Recurring Donation Charge Failed + when: + - c: System/Flag Recurring Payment Failed + then: + - e: System/Recurring Payment Failed + RecurringDonationCancelled: + when: + - c: Member/Cancel Recurring Donation + then: + - e: Member/Recurring Donation Cancelled + # [PLANNED] GDPR/SAR handling (scoped 2026-07-30, gap analysis done + # 2026-07-24). Chapter A (ed0643fd-...): a tracked, deadline-bound SAR + # process. "Approve Erasure Request" bridges into Identity's *existing* + # "Request Account Deletion" command (reused, not duplicated). "Flag + # Request Overdue" is an automation mirroring ShelterAdoption's existing + # MarkApplicationStale time-based pattern. Access fulfillment is a manual + # admin compile step for v1, not an automated per-module data-compilation + # fan-out (deferred as unnecessary complexity, AskUserQuestion decision). + HandlingDataSubjectRequests: + steps: + - t: Member/Submit Data Subject Request + - c: Member/Submit Data Subject Request + props: + requestType: Access | Rectification | Erasure + - e: Member/Data Subject Request Submitted + - t: Admin/Log Data Subject Request + - c: Admin/Log Data Subject Request + props: + requestType: Access | Rectification | Erasure + channel: Email | Phone | Post + - e: Admin/Data Subject Request Logged + - v: Admin/Data Subject Requests Queue + - t: Admin/Start Reviewing Request + - c: Admin/Start Reviewing Request + - e: Admin/Data Subject Request Review Started + - t: Admin/Approve Erasure Request + - c: Admin/Approve Erasure Request + - e: Admin/Erasure Request Approved + - c: System/Request Account Deletion + props: + triggeredBy: Erasure Request Approved + crossReference: AccountProfileSettings (existing command, reused not duplicated) + - e: System/Account Deletion Requested + - t: Admin/Compile Data Export + - c: Admin/Compile Data Export + - e: Admin/Data Export Compiled + - t: Admin/Apply Rectification + - c: Admin/Apply Rectification + props: + crossReference: whichever existing per-module edit command covers the field + - e: Admin/Rectification Applied + - t: Admin/Fulfil Request + - c: Admin/Fulfil Request + - e: Admin/Data Subject Request Fulfilled + - t: Admin/Reject Request + - c: Admin/Reject Request + props: + reason: Unable to verify requester identity + - e: Admin/Data Subject Request Rejected + - c: System/Flag Request Overdue + props: + triggeredBy: same time-based pattern as ShelterAdoption's MarkApplicationStale + - e: System/Data Subject Request Overdue Flagged + tests: + DataSubjectRequestSubmitted: + when: + - c: Member/Submit Data Subject Request + then: + - e: Member/Data Subject Request Submitted + DataSubjectRequestLogged: + when: + - c: Admin/Log Data Subject Request + then: + - e: Admin/Data Subject Request Logged + DataSubjectRequestsQueue: + given: + - e: Member/Data Subject Request Submitted + then: + - v: Admin/Data Subject Requests Queue + DataSubjectRequestReviewStarted: + given: + - e: Member/Data Subject Request Submitted + when: + - c: Admin/Start Reviewing Request + then: + - e: Admin/Data Subject Request Review Started + ErasureRequestApproved: + given: + - e: Admin/Data Subject Request Review Started + when: + - c: Admin/Approve Erasure Request + then: + - e: Admin/Erasure Request Approved + AccountDeletionRequestedFromErasure: + given: + - e: Admin/Erasure Request Approved + when: + - c: System/Request Account Deletion + then: + - e: System/Account Deletion Requested + DataExportCompiled: + given: + - e: Admin/Data Subject Request Review Started + when: + - c: Admin/Compile Data Export + then: + - e: Admin/Data Export Compiled + RectificationApplied: + given: + - e: Admin/Data Subject Request Review Started + when: + - c: Admin/Apply Rectification + then: + - e: Admin/Rectification Applied + DataSubjectRequestFulfilled: + given: + - e: Admin/Data Export Compiled + when: + - c: Admin/Fulfil Request + then: + - e: Admin/Data Subject Request Fulfilled + DataSubjectRequestRejected: + given: + - e: Admin/Data Subject Request Review Started + when: + - c: Admin/Reject Request + then: + - e: Admin/Data Subject Request Rejected + DataSubjectRequestOverdueFlagged: + given: + - e: Admin/Data Subject Request Logged + when: + - c: System/Flag Request Overdue + then: + - e: System/Data Subject Request Overdue Flagged + # [PLANNED] GDPR/SAR handling, chapter B (20ad9318-...). All three columns + # triggered by the same "Account Permanently Deleted" event (reusing that + # exact title from AccountProfileSettings' existing grace-period deletion + # column as the automation trigger label, same convention as + # BookingAppointments' 3-purpose Propose Appointment triggers). The real + # code gap this needs first: OwnerAccountPermanentlyDeletedV1 is + # Identity-internal only today, never promoted to Identity.Contracts. + ErasureCascade: + steps: + - c: System/Anonymize Application Data + props: + triggeredBy: Account Permanently Deleted + module: ShelterAdoption + - e: System/Application Data Anonymized + - c: System/Purge Notification History + props: + triggeredBy: Account Permanently Deleted + module: Notifications + - e: System/Notification History Purged + - c: System/Remove Owner's Media Assets + props: + triggeredBy: Account Permanently Deleted + module: Media + - e: System/Media Assets Removed + tests: + ApplicationDataAnonymized: + when: + - c: System/Anonymize Application Data + then: + - e: System/Application Data Anonymized + NotificationHistoryPurged: + when: + - c: System/Purge Notification History + then: + - e: System/Notification History Purged + MediaAssetsRemoved: + when: + - c: System/Remove Owner's Media Assets + then: + - e: System/Media Assets Removed diff --git a/build-kit-dotnet-es/README.md b/build-kit-dotnet-es/README.md index beece13..8e6856a 100644 --- a/build-kit-dotnet-es/README.md +++ b/build-kit-dotnet-es/README.md @@ -204,12 +204,16 @@ Each instance's git worktree lives as a sibling directory (`-ralph-1`, `-ralph-2`, ...) on branch `ralph/instance-N`, off whatever branch you were on when you ran the command. Once every tracked slice in the chapter reaches `Done` or `Blocked`, each instance's -branch is merged back automatically and its worktree removed. Per-instance -output goes to `ralph-1.log`/`ralph-2.log`/... in this directory (not the -terminal) — `orchestrate.mjs` itself only prints its own retrofit/flip/ -watch progress. Per-slice `InProgress`→terminal timing is appended to -`slice-timings.jsonl` as it happens, plus a running average printed at the -end of each watch. +branch is merged back automatically; its worktree is only removed if it's +actually clean afterward (`git status --porcelain` empty) — a Ralph loop +can pick up a new Planned slice from elsewhere on the board the instant its +tracked one finishes, so the worktree is left in place with instructions +printed if it still has uncommitted changes, rather than force-deleted. +Per-instance output goes to `ralph-1.log`/`ralph-2.log`/... in this +directory (not the terminal) — `orchestrate.mjs` itself only prints its own +retrofit/flip/watch progress. Per-slice `InProgress`→terminal timing is +appended to `slice-timings.jsonl` as it happens, plus a running average +printed at the end of each watch. Two `.eventmodelers/config.json` fields (optional, read by `lib/ralph.js`, not by `orchestrate.mjs` itself) matter more once you're running several diff --git a/build-kit-dotnet-es/lib/ralph.js b/build-kit-dotnet-es/lib/ralph.js index 26cd24f..05c3276 100644 --- a/build-kit-dotnet-es/lib/ralph.js +++ b/build-kit-dotnet-es/lib/ralph.js @@ -275,6 +275,14 @@ function readCurrentContext(kitDir) { try { return JSON.parse(readFileSync(ctxPath, 'utf-8')).name || null; } catch { return null; } } +// Dropped by orchestrate.mjs (in the worktree it passes as projectDir) once +// it decides a chapter is complete and is about to merge+remove that +// worktree — tells this loop to stop grabbing new Planned slices so it can't +// leave fresh uncommitted work sitting in a worktree that's about to be +// cleaned up. Doesn't interrupt a `claude -p` call already in flight; the +// orchestrator's own git-status check before removal is the real backstop. +const RALPH_STOP_FILE = '.ralph-stop'; + // Returns the first Planned slice IN THE CURRENT CONTEXT ONLY. If the current // context has no planned work, returns null so the loop waits — it must NEVER // cross into another context to find something to build. @@ -346,11 +354,12 @@ async function runWithRetry(label, fn, { maxAttempts = 3, onGiveUp } = {}) { } } -async function ralphLoop(kitDir, cfg, onTask, onPlannedSlice) { +async function ralphLoop(kitDir, projectDir, cfg, onTask, onPlannedSlice) { const promptFile = join(kitDir, 'lib', 'prompt.md'); const backendPromptFile = join(kitDir, 'lib', 'backend-prompt.md'); const credentialed = hasCredentials(cfg); let lastIdleCtx; + let stopLogged = false; let slicesBuilt = 0; const maxSlicesPerRun = cfg.maxSlicesPerRun ? parseInt(cfg.maxSlicesPerRun, 10) : null; @@ -372,7 +381,12 @@ async function ralphLoop(kitDir, cfg, onTask, onPlannedSlice) { didWork = true; } - const planned = onPlannedSlice && getFirstPlannedSlice(kitDir); + const stopSignaled = existsSync(join(projectDir, RALPH_STOP_FILE)); + if (stopSignaled && !stopLogged) { + console.log(`[ralph] Stop signal found at ${join(projectDir, RALPH_STOP_FILE)} — orchestrator is cleaning up this worktree, not picking up new planned slices.`); + stopLogged = true; + } + const planned = !stopSignaled && onPlannedSlice && getFirstPlannedSlice(kitDir); if (planned) { const prompt = readFileSync(backendPromptFile, 'utf-8').replaceAll('build-kit-dotnet-es', kitDir); await runWithRetry(`onPlannedSlice: building slice "${planned.title}"...`, () => onPlannedSlice(prompt), { @@ -421,7 +435,7 @@ export async function startRalph({ kitDir, projectDir, onTask, onPlannedSlice }) if (!hasCredentials(local)) { console.log(` mode: local-only (no platform sync)\n`); - await ralphLoop(kitDir, local, onTask, onPlannedSlice); + await ralphLoop(kitDir, projectDir, local, onTask, onPlannedSlice); return; } @@ -430,6 +444,6 @@ export async function startRalph({ kitDir, projectDir, onTask, onPlannedSlice }) await Promise.all([ startRealtimeAgent(cfg, kitDir), - ralphLoop(kitDir, cfg, onTask, onPlannedSlice), + ralphLoop(kitDir, projectDir, cfg, onTask, onPlannedSlice), ]); } diff --git a/build-kit-dotnet-es/orchestrate.mjs b/build-kit-dotnet-es/orchestrate.mjs index 86b9070..4eb255e 100644 --- a/build-kit-dotnet-es/orchestrate.mjs +++ b/build-kit-dotnet-es/orchestrate.mjs @@ -24,7 +24,7 @@ // after killing instances early, before a chapter finished. import { spawn, execFileSync } from 'child_process'; -import { readFileSync, existsSync, openSync, appendFileSync } from 'fs'; +import { readFileSync, existsSync, openSync, appendFileSync, writeFileSync } from 'fs'; import { dirname, resolve, join, basename } from 'path'; import { fileURLToPath } from 'url'; import { randomUUID } from 'crypto'; @@ -248,6 +248,38 @@ function worktreeDir(n) { return join(dirname(projectDir), `${basename(projectDir)}-ralph-${n}`); } +// A ralph loop runs detached and can pick up a brand-new Planned slice (from +// any chapter, not just the one this orchestrate.mjs run is watching — see +// getFirstPlannedSlice's "current context" scoping in lib/ralph.js) the +// instant its previously-tracked slice goes terminal, racing against +// monitorChapter deciding the chapter is done and starting cleanup. Dropping +// this file tells the loop in that specific worktree to stop grabbing new +// planned-slice work (checked once per loop iteration — it won't interrupt +// an already-running `claude -p` call). It's a best-effort narrowing of the +// race window, not a substitute for worktreeHasUncommittedChanges() below, +// which is the actual backstop against data loss. +const RALPH_STOP_FILE = '.ralph-stop'; + +function signalStop(dir, reason) { + try { + writeFileSync(join(dir, RALPH_STOP_FILE), `${reason}\n`, 'utf-8'); + } catch (err) { + console.error(` ! failed to write stop signal to ${dir}: ${err.message}`); + } +} + +// git worktree remove (without --force) already refuses to touch a worktree +// with uncommitted or untracked changes — that's git protecting the caller. +// Check status ourselves first (rather than blindly retrying with --force on +// ANY failure) so we can tell an in-flight-work refusal apart from some other +// removal failure, and never destroy real work just because a Ralph instance +// happened to pick up new work in the gap between its tracked slice going +// terminal and this cleanup step running. +function worktreeHasUncommittedChanges(dir) { + const status = execFileSync('git', ['status', '--porcelain'], { cwd: dir, encoding: 'utf-8' }); + return status.trim().length > 0; +} + function ensureWorktree(n, startPoint) { const dir = worktreeDir(n); const branch = `ralph/instance-${n}`; @@ -281,10 +313,15 @@ async function mergeWorktrees(n, targetBranch) { continue; } if (existsSync(dir)) { + if (worktreeHasUncommittedChanges(dir)) { + console.error(` ! ${dir} still has uncommitted/untracked changes after merging ${branch} — NOT removing it. This usually means the Ralph loop in this worktree picked up a new slice after ${branch} was merged. Inspect and clean up manually once you've confirmed nothing is lost:\n cd "${dir}" && git status\n git add -A && git commit # if the new work should be kept\n git worktree remove --force "${dir}"\n git branch -d "${branch}"\n (leaving this worktree/branch in place; not touching later instances)`); + continue; + } try { git(['worktree', 'remove', dir]); - } catch { - git(['worktree', 'remove', '--force', dir]); + } catch (err) { + console.error(` ! ${dir} reported clean but "git worktree remove" still failed (${err.message}) — leaving it in place rather than force-removing blindly. Investigate manually.`); + continue; } } try { @@ -436,6 +473,10 @@ async function main() { const { completed } = await monitorChapter(chapter, sliceIds); if (completed) { + for (let i = 1; i <= parallel; i++) { + const dir = worktreeDir(i); + if (existsSync(dir)) signalStop(dir, `chapter "${chapter.meta.title}" completed at ${new Date().toISOString()} — orchestrator is merging this worktree, not picking up new work`); + } await mergeWorktrees(parallel, startBranch); } else { console.log(`\nNot merging worktree branches yet — the Ralph instance(s) are still running (this is just the watch loop giving up after ${timeoutMinutes}m). Re-run with --watch to resume monitoring, or with --merge once you've stopped them, to merge whatever they finished.`); diff --git a/code/K9Crush-scaffold/K9Crush/Directory.Packages.props b/code/K9Crush-scaffold/K9Crush/Directory.Packages.props index eb52314..168f1dc 100644 --- a/code/K9Crush-scaffold/K9Crush/Directory.Packages.props +++ b/code/K9Crush-scaffold/K9Crush/Directory.Packages.props @@ -24,18 +24,18 @@ --> - - + + - - - - - + + + + + - - - + + + @@ -103,7 +103,7 @@ FetchForWriting call from an accidental LoadAsync call against the same type, since both reference an identical generic type argument; this needs an actual instruction-level scan. --> - + diff --git a/code/K9Crush-scaffold/K9Crush/docs/04-high-level-design.md b/code/K9Crush-scaffold/K9Crush/docs/04-high-level-design.md index ce465e9..d208330 100644 --- a/code/K9Crush-scaffold/K9Crush/docs/04-high-level-design.md +++ b/code/K9Crush-scaffold/K9Crush/docs/04-high-level-design.md @@ -2,7 +2,7 @@ > **This document's module inventory is stale and not the current source of > truth** - it predates several rounds of scope changes and only covers 8 -> of the modules that actually exist or are planned. `Spec/K9CRUSH.emlang.v3.yaml` +> of the modules that actually exist or are planned. `Spec/K9CRUSH.emlang.v3.2026-07-31.yaml` > (its own header, plus the "SCOPE NOTE" block added 2026-07-23) is the > real source of truth for module boundaries and current scope. As of > 2026-07-23, the product itself is being descoped away from a "dog dating