feat: wanted→offer matching, shown after posting#953
Open
edwh wants to merge 5 commits into
Open
Conversation
✅ Deploy Preview for golden-caramel-d2c3a7 ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
e930c53 to
50b6a4b
Compare
dfd2b96 to
387e215
Compare
…urement - GET /message/:id/similar: stored-embedding cosine, same-type, excludes source+own-author, MinSimilarScore, reach-filtered (shared ReachBlockedSet helper, also used by the browse read path), FEATURE_SIMILAR_POSTS killswitch. - Source-tagged impressions via MarkSeen (first-touch), threaded through the FE. - SimilarPosts.vue: lazy strip on the message page reusing MessageSummary, 10% userid holdout, >=3-results gate, one source-tagged impression per view. - Recommendations sysadmin tab: per-source impressions/clicks/CTR/attributed replies (7-day window) + holdout-vs-shown reply-rate comparison.
…shing message page)
The v-observe-visibility binding used {callback, once, intersection} but the
plugin expects {callback, options}. With no 'options' key it read
options.observeFullElement off undefined at beforeMount, throwing
'Cannot read properties of undefined (reading observeFullElement)' and turning
every message page into a 500 error page (caught by Playwright, not vitest which
stubs the directive). Match OurMessage.vue's proven shape; this plugin has no
'once' so guard re-entry with a loaded flag; add .catch() to the fire-and-forget
markSeen (review finding).
- GET /message/matches: embeds the wanted's item text, finds Offers within ~15km of the poster's chosen location, reach-filtered against that location (shared ReachBlockedSet), excludes own posts, FEATURE_WANTED_MATCH killswitch. - WantedMatches.vue: async, non-blocking panel under the postcode chooser on the desktop and mobile compose steps; reuses MessageSummary; opens each match in a new tab so the draft is preserved; dismissable. Impressions tagged wanted_match (already in the recommendations funnel). - Every failure path (off / no location / no match / sidecar down) hides the panel; it can never break or block posting.
Addresses the Phase 3 review: a monotonic load token discards an out-of-order response from a superseded query/location, and the fire-and-forget markSeen now .catch()es so a non-401 rejection never surfaces as an unhandled promise.
Per review: don't distract the poster mid-compose. Remove the WantedMatches panel from the find whereami step (desktop + mobile) and instead show it on the myposts landing page the poster is sent to right after posting - but only for a just-posted WANTED (derived from the posted item + location in history state). Updates the dismiss copy for the post-success context and adds myposts tests covering the WANTED-shows / OFFER-hides cases.
387e215 to
62ec367
Compare
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
Phase 3 of the vector-search rollout: while someone composes a WANTED, show them existing OFFERs near them that match — so they can grab something that's already available instead of waiting.
GET /message/matches— given the item text of a wanted-in-progress and the poster's chosen location, returns existing OFFERs within ~15km that semantically match. Because the wanted isn't saved yet, this embeds the query text via the sidecar (unlike/similar, which reuses a stored embedding), then does the in-memory cosine scan filtered to Offers in a lat/lng box around the location. It applies a search-gradeMinVectorScore(this is a match claim, not exploration), reach-filters against the poster's chosen location (a matched offer they couldn't reply to is never shown — reusing Phase 2's sharedReachBlockedSet), and excludes the poster's own offers. Behind aFEATURE_WANTED_MATCH=offkillswitch.WantedMatches.vuerenders under the postcode chooser on both the desktop (find/whereami.vue) and mobile (find/mobile/whereami.vue) compose steps once a location is chosen. It's async, emits nothing that gates the Next button, and opens each match in a new tab (window.open(..., '_blank')) so the in-progress draft is never lost. A dismiss link ("Not what I'm looking for - keep posting") collapses it. Even one match is worth showing, so it appears with ≥1 result.wanted_match): impressions when the panel shows, clicks when a card is opened, and attributed replies within 7 days — all already visible on the ModTools "Recommendations" sysadmin tab.Failure-safety
The compose flow must never break because matching couldn't run. Every failure path (feature off, no location, empty query, sidecar/embedding unavailable, no matches) returns an empty list and the panel simply hides — there is no path that errors or blocks the poster.
Code Quality Review
(To be completed by the code-review pass before merge.) Reuses Phase 2's shared
ReachBlockedSetand theMessageSummarycard; the new-tab open via@expandavoids nested-anchor issues while preserving the draft; the route is registered before/message/:idsso "matches" isn't parsed as a message id.Future Improvements
Test Plan
TestMatches*(5): returns nearby same-type offers (excludes Wanteds and out-of-box offers), flag-off empty, no-location empty, excludes the caller's own offers, reach-filtered (out-of-reach dropped, no-reach-row kept).WantedMatches.spec.js6✓ (no render on 0 matches, renders + calls matches with query+location, impression counted once aswanted_match, opens in a new tab taggedwanted_match, dismiss hides, no fetch without a location). Full suite green.Rollback:
FEATURE_WANTED_MATCH=offdisables the panel server-side (no deploy).