feat(FN-4777): complete Step 3 — wire GitHub tracking dedup pre-check

Fusion-Task-Id: FN-4777
Fusion-Task-Lineage: ed00f639-c928-4c14-be1f-2b43db6022c7
This commit is contained in:
Fusion (runfusion.ai)
2026-05-16 12:36:38 -07:00
committed by gsxdsm
parent de5a5c91d4
commit f60dde01fe
3 changed files with 84 additions and 0 deletions

View File

@@ -2858,6 +2858,9 @@ export interface ProjectSettings {
/** Project default GitHub tracking repo in `owner/repo` format (FN-3868).
* Falls back to global githubTrackingDefaultRepo when unset. */
githubTrackingDefaultRepo?: string;
/** When true, tracking issue creation searches open/closed repo issues for likely duplicates before opening a new issue.
* Default: true (set false to opt out). */
githubTrackingDedupEnabled?: boolean;
/** GitHub auth strategy for issue-tracking API calls in this project (FN-3868).
* Default: "gh-cli". */
githubAuthMode?: GithubAuthMode;

View File

@@ -11,6 +11,13 @@ import {
import type { CreatedIssue } from "./github.js";
import { GitHubClient } from "./github.js";
import { resolveGithubTrackingAuth } from "./github-auth.js";
import {
buildIssueSearchQueries,
DEDUP_MATCH_THRESHOLD,
extractFileScopePaths,
extractSymptomKeywords,
scoreCandidateIssue,
} from "./github-tracking-dedup.js";
const TRACKING_ISSUE_TITLE_LIMIT = 240;
const TRACKING_ISSUE_BODY_SUMMARY_LIMIT = 500;
@@ -144,6 +151,7 @@ export type MaybeCreateTrackingIssueReason =
| "issue_already_linked"
| "no_repo_configured"
| "no_title_available"
| "existing_issue_found"
| "github_error"
| "auth_token_missing"
| "auth_gh_not_installed"
@@ -311,6 +319,72 @@ export async function maybeCreateTrackingIssue(
const title = formatTrackingIssueTitle(latestTask);
const body = formatTrackingIssueBody(latestTask);
if (deps.projectSettings.githubTrackingDedupEnabled !== false) {
try {
const paths = extractFileScopePaths(latestTask as Task & { prompt?: string });
const keywords = extractSymptomKeywords(latestTask, { max: 6 });
if (paths.length > 0 || keywords.length > 0) {
const queries = buildIssueSearchQueries(paths, keywords);
const byNumber = new Map<number, {
number: number;
title: string;
body: string | null;
html_url: string;
state: "open" | "closed";
updatedAt?: string;
}>();
for (const query of queries) {
const candidates = await githubClient.searchIssues(repo.owner, repo.repo, query, { state: "all", limit: 10 });
for (const candidate of candidates) {
if (!byNumber.has(candidate.number)) {
byNumber.set(candidate.number, candidate);
}
}
const scored = [...byNumber.values()]
.map((candidate) => ({ candidate, ...scoreCandidateIssue(candidate, paths, keywords) }))
.filter((entry) => entry.score >= DEDUP_MATCH_THRESHOLD)
.filter((entry) => entry.matchedPaths.length > 0 || entry.matchedKeywords.length >= 2)
.sort((a, b) => b.score - a.score);
const bestMatch = scored[0];
if (bestMatch) {
await deps.taskStore.linkGithubIssue(task.id, {
owner: repo.owner,
repo: repo.repo,
number: bestMatch.candidate.number,
url: bestMatch.candidate.html_url,
createdAt: bestMatch.candidate.updatedAt ?? new Date().toISOString(),
});
await deps.taskStore.recordActivity({
type: "task:updated",
taskId: task.id,
taskTitle: latestTask.title,
details: `Linked existing issue ${repo.owner}/${repo.repo}#${bestMatch.candidate.number} (dedup match; see docs/triage-duplicate-detection-postmortem.md)`,
metadata: {
type: "github-issue-dedup-matched",
repo: `${repo.owner}/${repo.repo}`,
number: bestMatch.candidate.number,
htmlUrl: bestMatch.candidate.html_url,
score: bestMatch.score,
matchedPaths: bestMatch.matchedPaths,
matchedKeywords: bestMatch.matchedKeywords,
state: bestMatch.candidate.state,
},
});
return { created: false, reason: "existing_issue_found" };
}
}
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
deps.logger?.warn?.(`[github-tracking] ${task.id}: duplicate-search failed; falling back to issue creation: ${message}`);
}
}
try {
const issue = await githubClient.createIssue({ owner: repo.owner, repo: repo.repo, title, body });

View File

@@ -13,6 +13,13 @@ export {
export { createSkillsAdapter, getProjectSettingsPath, type SkillsAdapter, type DiscoveredSkill, type CatalogEntry, type CatalogFetchResult, type ToggleSkillResult, type UpstreamError, type UpstreamErrorCode, type SkillContent, type SkillFileEntry } from "./skills-adapter.js";
export { GitHubClient, isPrMergeReady, type GitHubClientOptions, type PrMergeStatus, type PrCheckStatus, type ReviewDecision, type MergePrParams, type FindPrParams, type CreateIssueParams, type CreatedIssue } from "./github.js";
export { maybeCreateTrackingIssue, type MaybeCreateTrackingIssueDeps } from "./github-tracking.js";
export {
buildIssueSearchQueries,
DEDUP_MATCH_THRESHOLD,
extractFileScopePaths,
extractSymptomKeywords,
scoreCandidateIssue,
} from "./github-tracking-dedup.js";
export { registerGithubTrackingHook } from "./github-tracking-hook.js";
export {
resolveGithubTrackingAuth,