Skip to content

Commit a2a307f

Browse files
committed
feat: add generic method to fetch issues by project, document and status
- Add findByProjectDocumentAndStatus method to IssuesRepository - Method accepts IssueStatus parameter (active/inactive) for flexibility - Implement buildStatusCondition helper to build SQL conditions based on status - Update filterOutInactiveCandidates to use the new generic method - Remove unused imports from discover.ts
1 parent 554bba7 commit a2a307f

File tree

3 files changed

+173
-6
lines changed

3 files changed

+173
-6
lines changed

packages/core/src/repositories/issuesRepository.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
ISSUE_STATUS,
66
IssueGroup,
77
IssueSort,
8+
IssueStatus,
89
SafeIssuesParams,
910
} from '@latitude-data/constants/issues'
1011
import {
@@ -227,6 +228,29 @@ export class IssuesRepository extends Repository<Issue> {
227228
.limit(20)
228229
}
229230

231+
async findByProjectDocumentAndStatus({
232+
project,
233+
documentUuid,
234+
status,
235+
}: {
236+
project: Project
237+
documentUuid: string
238+
status: IssueGroup
239+
}) {
240+
const statusCondition = this.buildGroupConditions(status)
241+
return this.db
242+
.select({ uuid: issues.uuid })
243+
.from(issues)
244+
.where(
245+
and(
246+
this.scopeFilter,
247+
eq(issues.projectId, project.id),
248+
eq(issues.documentUuid, documentUuid),
249+
statusCondition,
250+
),
251+
)
252+
}
253+
230254
async fetchIssuesFiltered({
231255
project,
232256
commit,

packages/core/src/services/issues/discover.test.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@ import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
44
import { SpanType } from '../../constants'
55
import { createEvaluationResultV2 } from '../../tests/factories/evaluationResultsV2'
66
import { createEvaluationV2 } from '../../tests/factories/evaluationsV2'
7+
import { createIssue } from '../../tests/factories/issues'
78
import { createProject } from '../../tests/factories/projects'
89
import { createSpan } from '../../tests/factories/spans'
10+
import { createUser } from '../../tests/factories/users'
911
import { createWorkspace } from '../../tests/factories/workspaces'
1012
import * as weaviate from '../../weaviate'
1113
import { discoverIssue } from './discover'
14+
import { ignoreIssue } from './ignore'
1215
import * as sharedModule from './shared'
1316

1417
vi.mock('../../voyage', () => ({
@@ -383,5 +386,124 @@ describe('discoverIssue', () => {
383386
expect(embedding).toBeDefined()
384387
expect(issue).toBeUndefined()
385388
})
389+
390+
it('filters out inactive candidates from discovery results', async () => {
391+
const { workspace } = await createWorkspace({ features: ['issues'] })
392+
const projectResult = await createProject({
393+
workspace,
394+
documents: {
395+
'test-prompt': 'This is a test prompt',
396+
},
397+
})
398+
const { project, commit, documents } = projectResult
399+
const document = documents[0]!
400+
401+
const evaluation = await createEvaluationV2({
402+
document,
403+
commit,
404+
workspace,
405+
})
406+
407+
const span = await createSpan({
408+
workspaceId: workspace.id,
409+
documentUuid: document.documentUuid,
410+
commitUuid: commit.uuid,
411+
type: SpanType.Prompt,
412+
})
413+
414+
const result = await createEvaluationResultV2({
415+
evaluation,
416+
span,
417+
commit,
418+
workspace,
419+
hasPassed: false,
420+
})
421+
422+
// Create an active issue
423+
const { issue: activeIssue } = await createIssue({
424+
workspace,
425+
project,
426+
document,
427+
createdAt: new Date(),
428+
histograms: [
429+
{
430+
commitId: commit.id,
431+
date: new Date(),
432+
count: 1,
433+
},
434+
],
435+
})
436+
437+
// Create an inactive issue (ignored)
438+
const { issue: inactiveIssue } = await createIssue({
439+
workspace,
440+
project,
441+
document,
442+
createdAt: new Date(),
443+
histograms: [
444+
{
445+
commitId: commit.id,
446+
date: new Date(),
447+
count: 1,
448+
},
449+
],
450+
})
451+
452+
const user = await createUser()
453+
await ignoreIssue({ issue: inactiveIssue, user }).then((r) => r.unwrap())
454+
455+
const mockEmbedding = Array(2048).fill(0.1)
456+
vi.spyOn(sharedModule, 'embedReason').mockResolvedValue({
457+
ok: true,
458+
value: mockEmbedding,
459+
unwrap: () => mockEmbedding,
460+
} as any)
461+
462+
// Mock Weaviate to return both active and inactive issues as candidates
463+
const mockHybrid = vi.fn().mockResolvedValue({
464+
objects: [
465+
{
466+
uuid: activeIssue.uuid,
467+
properties: {
468+
title: activeIssue.title,
469+
description: activeIssue.description,
470+
},
471+
metadata: { score: 0.95 },
472+
},
473+
{
474+
uuid: inactiveIssue.uuid,
475+
properties: {
476+
title: inactiveIssue.title,
477+
description: inactiveIssue.description,
478+
},
479+
metadata: { score: 0.9 },
480+
},
481+
],
482+
})
483+
484+
const mockCollection = {
485+
query: {
486+
hybrid: mockHybrid,
487+
},
488+
}
489+
490+
vi.spyOn(weaviate, 'getIssuesCollection').mockResolvedValue(
491+
mockCollection as any,
492+
)
493+
494+
const discovering = await discoverIssue({
495+
result: { result, evaluation },
496+
document,
497+
project,
498+
})
499+
500+
expect(discovering.error).toBeFalsy()
501+
const { embedding, issue } = discovering.unwrap()
502+
expect(embedding).toBeDefined()
503+
// Should only return the active issue, not the inactive one
504+
expect(issue).toBeDefined()
505+
expect(issue?.uuid).toBe(activeIssue.uuid)
506+
expect(issue?.uuid).not.toBe(inactiveIssue.uuid)
507+
})
386508
})
387509
})

packages/core/src/services/issues/discover.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { env } from '@latitude-data/env'
2+
import { ISSUE_STATUS } from '@latitude-data/constants/issues'
23
import { Bm25Operator } from 'weaviate-client'
34
import { cache as getCache } from '../../cache'
45
import { database } from '../../client'
@@ -19,6 +20,7 @@ import {
1920
import { UnprocessableEntityError } from '../../lib/errors'
2021
import { hashContent } from '../../lib/hashContent'
2122
import { Result, TypedResult } from '../../lib/Result'
23+
import { IssuesRepository } from '../../repositories'
2224
import { type DocumentVersion } from '../../schema/models/types/DocumentVersion'
2325
import { type Project } from '../../schema/models/types/Project'
2426
import { type ResultWithEvaluationV2 } from '../../schema/types'
@@ -76,7 +78,12 @@ export async function discoverIssue<
7678

7779
embedding = normalizeEmbedding(embedding)
7880

79-
const finding = await findCandidates({ reason, embedding, document, project })
81+
const finding = await findCandidates({
82+
reason,
83+
embedding,
84+
document,
85+
project,
86+
})
8087
if (finding.error) {
8188
return Result.error(finding.error)
8289
}
@@ -114,11 +121,23 @@ async function findCandidates({
114121
document: DocumentVersion
115122
project: Project
116123
}) {
124+
async function filterOutInactiveCandidates(
125+
candidates: IssueCandidate[],
126+
): Promise<IssueCandidate[]> {
127+
const issuesRepo = new IssuesRepository(project.workspaceId)
128+
const inactiveIssues = await issuesRepo.findByProjectDocumentAndStatus({
129+
project,
130+
documentUuid: document.documentUuid,
131+
status: 'inactive',
132+
})
133+
const inactiveUuids = new Set(inactiveIssues.map((issue) => issue.uuid))
134+
return candidates.filter((candidate) => !inactiveUuids.has(candidate.uuid))
135+
}
136+
117137
try {
118138
const tenantName = ISSUES_COLLECTION_TENANT_NAME(project.workspaceId, project.id, document.documentUuid) // prettier-ignore
119-
const issues = await getIssuesCollection({ tenantName })
120-
121-
const { objects } = await issues.query.hybrid(reason, {
139+
const issuesCollection = await getIssuesCollection({ tenantName })
140+
const { objects } = await issuesCollection.query.hybrid(reason, {
122141
vector: embedding,
123142
alpha: ISSUE_DISCOVERY_SEARCH_RATIO,
124143
maxVectorDistance: 1 - ISSUE_DISCOVERY_MIN_SIMILARITY,
@@ -130,7 +149,6 @@ async function findCandidates({
130149
returnProperties: ['title', 'description'],
131150
returnMetadata: ['score'],
132151
})
133-
134152
const candidates = objects
135153
.map((object) => ({
136154
uuid: object.uuid,
@@ -139,8 +157,11 @@ async function findCandidates({
139157
score: object.metadata!.score!,
140158
}))
141159
.slice(0, ISSUE_DISCOVERY_MAX_CANDIDATES)
160+
if (candidates.length === 0) return Result.ok<IssueCandidate[]>([])
142161

143-
return Result.ok<IssueCandidate[]>(candidates)
162+
const activeCandidates = await filterOutInactiveCandidates(candidates)
163+
164+
return Result.ok<IssueCandidate[]>(activeCandidates)
144165
} catch (error) {
145166
return Result.error(error as Error)
146167
}

0 commit comments

Comments
 (0)