Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/generated/graphql/schema.executable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 6 additions & 0 deletions src/generated/graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions src/lib/feedback/brain.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
24 changes: 18 additions & 6 deletions src/lib/feedback/dedupe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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. */
Expand All @@ -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}
`);
Expand All @@ -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)
);
Expand Down
6 changes: 6 additions & 0 deletions src/lib/graphql/plugins/feedback/SignalIngestion.plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading