diff --git a/src/generated/graphql/schema.executable.ts b/src/generated/graphql/schema.executable.ts index 03bb2ae..9e07975 100644 --- a/src/generated/graphql/schema.executable.ts +++ b/src/generated/graphql/schema.executable.ts @@ -19801,11 +19801,17 @@ type PromoteSignalToPostPayload { projectId: UUID! } +type SimilarPostStatus { + displayName: String! + color: String +} + type SimilarPost { id: UUID! number: Int title: String score: Float! + status: SimilarPostStatus } input ChangePostStatusInput { diff --git a/src/generated/graphql/schema.graphql b/src/generated/graphql/schema.graphql index fea0e44..57acba7 100644 --- a/src/generated/graphql/schema.graphql +++ b/src/generated/graphql/schema.graphql @@ -11645,11 +11645,17 @@ type PromoteSignalToPostPayload { projectId: UUID! } +type SimilarPostStatus { + displayName: String! + color: String +} + type SimilarPost { id: UUID! number: Int title: String score: Float! + status: SimilarPostStatus } input ChangePostStatusInput { diff --git a/src/lib/feedback/brain.integration.test.ts b/src/lib/feedback/brain.integration.test.ts index 02a1135..be38131 100644 --- a/src/lib/feedback/brain.integration.test.ts +++ b/src/lib/feedback/brain.integration.test.ts @@ -172,6 +172,8 @@ describe.skipIf(!DATABASE_URL)("feedback brain (db integration)", () => { expect(matches.length).toBeGreaterThan(0); expect(matches[0].title).toBe("Add dark mode to settings"); expect(matches[0].number).not.toBeNull(); + // status is surfaced (null here: these posts carry no status template) + expect(matches[0].status).toBeNull(); }); test("returns nothing for empty content", async () => { diff --git a/src/lib/feedback/dedupe.ts b/src/lib/feedback/dedupe.ts index f49a180..313e02f 100644 --- a/src/lib/feedback/dedupe.ts +++ b/src/lib/feedback/dedupe.ts @@ -11,7 +11,7 @@ */ import { sql } from "drizzle-orm"; -import { posts } from "lib/db/schema"; +import { posts, statusTemplates } from "lib/db/schema"; import type { dbPool } from "lib/db/db"; @@ -110,6 +110,9 @@ interface SimilarPost { number: number | null; title: string | null; score: number; + // current status of the candidate, so the UI can show whether a match is + // already completed/closed rather than open. null when the post has no status + status: { displayName: string; color: string | null } | null; } /** Minimum similarity to surface a post as a possible duplicate to the user. */ @@ -130,14 +133,17 @@ export const findSimilarPosts = async ( if (!content.trim()) return []; const result = await db.execute(sql` - SELECT id, number, title, + SELECT p.id, p.number, p.title, + st.display_name AS status_display_name, + st.color AS status_color, similarity( - coalesce(title, '') || ' ' || coalesce(description, ''), + coalesce(p.title, '') || ' ' || coalesce(p.description, ''), ${content} ) AS score - FROM ${posts} - WHERE project_id = ${projectId} - AND duplicate_of_id IS NULL + FROM ${posts} p + LEFT JOIN ${statusTemplates} st ON st.id = p.status_template_id + WHERE p.project_id = ${projectId} + AND p.duplicate_of_id IS NULL ORDER BY score DESC LIMIT ${limit} `); @@ -150,6 +156,12 @@ export const findSimilarPosts = async ( number: row.number as number | null, title: row.title as string | null, score: Number(row.score), + status: row.status_display_name + ? { + displayName: row.status_display_name as string, + color: (row.status_color as string | null) ?? null, + } + : null, })) .filter((post) => post.score >= SIMILAR_POST_THRESHOLD) ); diff --git a/src/lib/graphql/plugins/feedback/SignalIngestion.plugin.ts b/src/lib/graphql/plugins/feedback/SignalIngestion.plugin.ts index 2ed3736..8eed203 100644 --- a/src/lib/graphql/plugins/feedback/SignalIngestion.plugin.ts +++ b/src/lib/graphql/plugins/feedback/SignalIngestion.plugin.ts @@ -65,11 +65,17 @@ const SignalIngestionPlugin = makeExtendSchemaPlugin(() => ({ projectId: UUID! } + type SimilarPostStatus { + displayName: String! + color: String + } + type SimilarPost { id: UUID! number: Int title: String score: Float! + status: SimilarPostStatus } extend type Mutation {