From da1e445d6b6b27ea2d54001b69f0228bbfd06ba8 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sun, 19 Jul 2026 14:51:49 -0700 Subject: [PATCH] FN-8310: add public-roadmap report deduplication Add public-roadmap issues as a configurable, reviewable deduplication source for in-app reports. - Resolve roadmap settings across project, global, and tracking-repository fallbacks - Search and endorse matching open roadmap issues before filing duplicate reports - Expose and index roadmap controls with localized default descriptions Files changed: .changeset/fn-8310-public-roadmap-dedupe.md | 7 ++ docs/dashboard-guide.md | 2 +- docs/settings-reference.md | 4 +- .../core/src/__tests__/settings-parity.test.ts | 14 +++- packages/core/src/settings-schema.ts | 7 +- packages/core/src/types.ts | 13 +++- packages/dashboard/app/api/report.ts | 2 +- packages/dashboard/app/components/ReportModal.tsx | 18 ++--- .../dashboard/app/components/SettingsModal.tsx | 15 +++- .../app/components/__tests__/ReportModal.test.tsx | 12 ++-- .../__tests__/SettingsModal.general.test.tsx | 6 +- .../settings/__tests__/section-keys.test.ts | 5 +- .../app/components/settings/save-split.ts | 14 ++-- .../app/components/settings/section-keys.ts | 5 ++ .../settings/sections/GeneralSection.search.ts | 34 ++++++--- .../settings/sections/GeneralSection.tsx | 29 ++++++-- .../sections/SourceControlGlobalSection.search.ts | 27 +++++++ .../sections/SourceControlGlobalSection.tsx | 55 ++++++++++---- .../settings-default-descriptions.test.tsx | 4 +- .../src/__tests__/report-pipeline.test.ts | 73 ++++++++++++------- .../dashboard/src/__tests__/report-routes.test.ts | 23 +++--- packages/dashboard/src/report-pipeline.ts | 84 ++++++++++++++-------- .../dashboard/src/routes/register-report-routes.ts | 9 +-- packages/i18n/locales/en/app.json | 17 ++++- packages/i18n/locales/es/app.json | 17 ++++- packages/i18n/locales/fr/app.json | 17 ++++- packages/i18n/locales/ko/app.json | 17 ++++- packages/i18n/locales/zh-CN/app.json | 16 ++++- packages/i18n/locales/zh-TW/app.json | 16 ++++- 29 files changed, 409 insertions(+), 153 deletions(-) Fusion-Task-Id: FN-8310 Fusion-Task-Lineage: 0efa8d76-e849-4256-bdcd-e9d6dd612e41 Co-authored-by: Fusion (runfusion.ai) --- .changeset/fn-8310-public-roadmap-dedupe.md | 7 ++ docs/dashboard-guide.md | 2 +- docs/settings-reference.md | 4 +- .../src/__tests__/settings-parity.test.ts | 14 +++- packages/core/src/settings-schema.ts | 7 +- packages/core/src/types.ts | 13 ++- packages/dashboard/app/api/report.ts | 2 +- .../dashboard/app/components/ReportModal.tsx | 18 ++-- .../app/components/SettingsModal.tsx | 15 +++- .../components/__tests__/ReportModal.test.tsx | 12 ++- .../__tests__/SettingsModal.general.test.tsx | 6 +- .../settings/__tests__/section-keys.test.ts | 5 +- .../app/components/settings/save-split.ts | 14 +++- .../app/components/settings/section-keys.ts | 5 ++ .../sections/GeneralSection.search.ts | 34 +++++--- .../settings/sections/GeneralSection.tsx | 29 +++++-- .../SourceControlGlobalSection.search.ts | 27 ++++++ .../sections/SourceControlGlobalSection.tsx | 55 +++++++++--- .../settings-default-descriptions.test.tsx | 4 +- .../src/__tests__/report-pipeline.test.ts | 73 ++++++++++------ .../src/__tests__/report-routes.test.ts | 23 ++--- packages/dashboard/src/report-pipeline.ts | 84 ++++++++++++------- .../src/routes/register-report-routes.ts | 9 +- packages/i18n/locales/en/app.json | 17 +++- packages/i18n/locales/es/app.json | 17 +++- packages/i18n/locales/fr/app.json | 17 +++- packages/i18n/locales/ko/app.json | 17 +++- packages/i18n/locales/zh-CN/app.json | 16 +++- packages/i18n/locales/zh-TW/app.json | 16 +++- 29 files changed, 409 insertions(+), 153 deletions(-) create mode 100644 .changeset/fn-8310-public-roadmap-dedupe.md diff --git a/.changeset/fn-8310-public-roadmap-dedupe.md b/.changeset/fn-8310-public-roadmap-dedupe.md new file mode 100644 index 0000000000..d2365f2bdd --- /dev/null +++ b/.changeset/fn-8310-public-roadmap-dedupe.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Deduplicate in-app reports against open public-roadmap issues. +category: feature +dev: Adds project/global roadmap label and repository resolution settings. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 2bea250152..142464cc2d 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -2121,7 +2121,7 @@ The Header **Report** menu is available on desktop and mobile and offers **Bug** Fusion gathers available task/agent context, structures the prompt into a report, scrubs secrets, local paths, project names, home-directory identities, email addresses, and likely personal names, then checks **open** GitHub issues or Discussions for duplicates. Scrubbing is mandatory for every route and is repeated on the server when a reviewed draft is edited before filing. A strong duplicate receives a visible 👍 reaction and one scrubbed data-point comment instead of a new issue or Discussion. Bug and Idea reports use issues; Feedback and unresolved Help reports use repository Discussions. If preparation or filing cannot reach GitHub, Fusion preserves the draft and shows a retryable error. -Projects can opt into **Check roadmap before filing reports** in **Settings → General**. It is off by default. When enabled, the project roadmap is a third deduplication source: a strong feature match is shown inline as **Already on the roadmap**, including its title and description, and no Issue, Discussion, comment, or reaction is filed. The result intentionally has no roadmap link because Fusion does not expose a roadmap dashboard destination. +The public roadmap is also a deduplication source. With `reportRoadmapDedupeEnabled` (default `true`), Fusion searches open GitHub Issues in `reportRoadmapRepo` (or the normal tracking repository when unset) carrying `reportRoadmapLabel` (default `roadmap`). This pass applies to Bug, Idea, Feedback, and Help reports, before ordinary Issue or Discussion matching. A matching roadmap issue wins deterministically: **Review draft** asks whether to add the scrubbed data point; **File automatically** adds the existing 👍 and scrubbed comment. Closed items and unavailable roadmap searches are ignored, so reports fall back to normal destination dedupe. The server rechecks the open label-qualified match before it endorses an item from an edited browser draft. In **Settings → General**, choose **Review draft before filing** (the default) or **File automatically**. Both paths show the resulting issue, Discussion, or endorsement link. Help checks Fusion's local knowledge index on every server report path first and only escalates when it cannot find an answer. diff --git a/docs/settings-reference.md b/docs/settings-reference.md index d9721c66fb..00531ae56a 100644 --- a/docs/settings-reference.md +++ b/docs/settings-reference.md @@ -656,7 +656,9 @@ Default notes: | `githubLinkImportedIssuesToTracking` | `boolean` | `false` | Project-scoped, import-only option. When enabled, GitHub issue imports from the dashboard, CLI, and extension tools persist `githubTracking: { enabled: true }` so Fusion adopts the imported source issue as the tracking issue without turning tracking on for ordinary new tasks. Duplicate/skipped imports do not create tasks or tracking metadata. | | `reportMode` | `"draft-review" \| "auto-file"` | `"draft-review"` | Default in-app Bug, Feedback, Idea, and Help report behavior. `"draft-review"` presents the scrubbed, structured draft for approval; `"auto-file"` files a new report or endorses a strong open duplicate automatically. Dashboard location: **Settings → Project → General → In-app report mode**. | | `reportModeByAction` | `Partial>` | `undefined` | Optional per-action override map. An action-specific value takes precedence over `reportMode`; unset actions use the project default. Every mode always scrubs report content before GitHub egress. | -| `reportRoadmapDedup` | `boolean` | `false` | Opt-in project roadmap deduplication for in-app reports. A strong roadmap feature match returns an inline `roadmap-match` result and performs no GitHub egress; off preserves Issues-and-Discussions-only behavior. Dashboard location: **Settings → Project → General → Check roadmap before filing reports**. | +| `reportRoadmapDedupeEnabled` | `boolean` | `true` | Enables open public-roadmap GitHub Issue matching before ordinary Issue/Discussion report dedupe. Project value overrides global; unavailable searches fall back safely. Dashboard location: **Settings → Project → General**. | +| `reportRoadmapLabel` | `string` | `"roadmap"` | Label qualifying open GitHub Issues as public roadmap items. Project value overrides global. | +| `reportRoadmapRepo` | `string` | `undefined` | Optional `owner/repo` public-roadmap repository. Project value overrides global; unset falls back to `githubTrackingDefaultRepo`, then `githubDefaultRepo`. | | `githubImportAutoTranslate` | `boolean` | `false` | Project-scoped, import-only option. When enabled, the Import Tasks panel automatically translates foreign-language **GitHub open issue** titles and bodies into `importTranslateTargetLocale` and shows the translation by default (the original text stays one toggle away). Translation follows each reachable page of the fetched list (up to the 300-issue fetch cap in one hour); cached repeat views do not spend model calls. Pull requests and GitLab retain per-selection translation. Translations persist across app restarts until the upstream issue changes or closes. Off by default so all-English projects never pay for a per-issue AI call. Issue-form template scaffolding (headings, field labels, placeholders, and checkboxes) is ignored when detecting an issue's language, so foreign-language answers remain eligible. Dashboard location: **Settings → Project → General → GitHub Tracking**. | | `importTranslateTargetLocale` | `Locale` | `undefined` | Target language for `githubImportAutoTranslate`. One of `SUPPORTED_LOCALES`. When unset, import translation follows the dashboard's own `language` setting. Dashboard location: **Settings → Project → General → GitHub Tracking**. | | `githubTrackingDefaultRepo` | `string` | `undefined` | Project default issue-tracking repo (`owner/repo`) used before global fallback for tracked task creation (precedence: task override → project default → global default). In Settings UI this is a detected-remote dropdown with a Custom fallback for manual entry. This key is dual-scope: project saves go through `PUT /api/settings` (Settings → General → GitHub Tracking) while global saves go through `PUT /api/settings/global` (Settings → Global General). | diff --git a/packages/core/src/__tests__/settings-parity.test.ts b/packages/core/src/__tests__/settings-parity.test.ts index 63e3b2e44c..aeec290929 100644 --- a/packages/core/src/__tests__/settings-parity.test.ts +++ b/packages/core/src/__tests__/settings-parity.test.ts @@ -169,13 +169,18 @@ describe("settings key parity", () => { it("defaults in-app reports to draft review and keeps mode settings project-scoped", () => { expect(DEFAULT_PROJECT_SETTINGS.reportMode).toBe("draft-review"); expect(DEFAULT_PROJECT_SETTINGS.reportModeByAction).toBeUndefined(); - expect(DEFAULT_PROJECT_SETTINGS.reportRoadmapDedup).toBe(false); + expect(DEFAULT_PROJECT_SETTINGS.reportRoadmapDedupeEnabled).toBe(true); + expect(DEFAULT_PROJECT_SETTINGS.reportRoadmapLabel).toBe("roadmap"); expect(isProjectSettingsKey("reportMode")).toBe(true); expect(isProjectSettingsKey("reportModeByAction")).toBe(true); - expect(isProjectSettingsKey("reportRoadmapDedup")).toBe(true); + expect(isProjectSettingsKey("reportRoadmapDedupeEnabled")).toBe(true); + expect(isProjectSettingsKey("reportRoadmapLabel")).toBe(true); + expect(isProjectSettingsKey("reportRoadmapRepo")).toBe(true); expect(isGlobalSettingsKey("reportMode")).toBe(false); expect(isGlobalSettingsKey("reportModeByAction")).toBe(false); - expect(isGlobalSettingsKey("reportRoadmapDedup")).toBe(false); + expect(isGlobalSettingsKey("reportRoadmapDedupeEnabled")).toBe(true); + expect(isGlobalSettingsKey("reportRoadmapLabel")).toBe(true); + expect(isGlobalSettingsKey("reportRoadmapRepo")).toBe(true); }); it("defaults autoClaimCandidatesInPrompt to 5 and keeps it project-scoped", () => { @@ -531,6 +536,9 @@ describe("settings key parity", () => { "mergeRequestContractShadowEnabled", "taskTokenBudget", "githubTrackingDefaultRepo", + "reportRoadmapDedupeEnabled", + "reportRoadmapLabel", + "reportRoadmapRepo", "gitlabEnabled", "gitlabInstanceUrl", "gitlabApiBaseUrl", diff --git a/packages/core/src/settings-schema.ts b/packages/core/src/settings-schema.ts index 47942e4305..4c521043eb 100644 --- a/packages/core/src/settings-schema.ts +++ b/packages/core/src/settings-schema.ts @@ -195,6 +195,9 @@ export const DEFAULT_GLOBAL_SETTINGS = { updateChannel: "stable", autoReloadOnVersionChange: true, githubTrackingDefaultRepo: undefined, + reportRoadmapDedupeEnabled: undefined, + reportRoadmapLabel: undefined, + reportRoadmapRepo: undefined, gitlabEnabled: undefined, gitlabInstanceUrl: undefined, gitlabApiBaseUrl: undefined, @@ -643,7 +646,9 @@ export const DEFAULT_PROJECT_SETTINGS = { githubTrackingDefaultRepo: undefined, reportMode: "draft-review" as const, reportModeByAction: undefined, - reportRoadmapDedup: false, + reportRoadmapDedupeEnabled: true, + reportRoadmapLabel: "roadmap", + reportRoadmapRepo: undefined, gitlabEnabled: undefined, gitlabInstanceUrl: undefined, gitlabApiBaseUrl: undefined, diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index a533ba99c8..c6f899ddd4 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -2871,6 +2871,10 @@ export interface GlobalSettings { /** Global fallback GitHub tracking repo in `owner/repo` format (FN-3868). * Used when a project has no githubTrackingDefaultRepo. */ githubTrackingDefaultRepo?: string; + /** Global fallback configuration for public-roadmap report deduplication. */ + reportRoadmapDedupeEnabled?: boolean; + reportRoadmapLabel?: string; + reportRoadmapRepo?: string; /** Global GitLab integration enable flag. Undefined is effectively enabled for backward compatibility; projects can override this value. */ gitlabEnabled?: boolean; /** Global fallback GitLab web instance URL. Defaults effectively to https://gitlab.com when unset. @@ -4219,10 +4223,13 @@ export interface ProjectSettings { reportModeByAction?: Partial>; /** * FNXC:ReportPipeline 2026-07-18-12:00: - * Roadmap deduplication is opt-in because projects without a roadmap must - * retain the existing Issues-and-Discussions-only report behavior. + * FR-30 public-roadmap dedupe is an additive GitHub Issue source. Effective + * values resolve project → global → built-in defaults and reuse tracking-repo + * resolution when no dedicated roadmap repo is configured. */ - reportRoadmapDedup?: boolean; + reportRoadmapDedupeEnabled?: boolean; + reportRoadmapLabel?: string; + reportRoadmapRepo?: string; /** * FNXC:GitLabConfiguration 2026-07-02-00:00: * FN-7422 adds durable GitLab instance/API URL settings for GitLab.com and self-managed hosts. FN-7423 layers token settings onto the same project-over-global configuration contract without adding runtime GitLab imports or tracking. diff --git a/packages/dashboard/app/api/report.ts b/packages/dashboard/app/api/report.ts index 6936e198ac..a457c8a81d 100644 --- a/packages/dashboard/app/api/report.ts +++ b/packages/dashboard/app/api/report.ts @@ -10,5 +10,5 @@ export interface ReportScreenshot { dataUrl: string; capturedAt: string; } export interface ReportContextInput { actionType: ReportActionType; userPrompt: string; contextRefs?: { taskId?: string; agentId?: string }; activityTrace?: string[]; screenshot?: ReportScreenshot; } export function reportDraft(input: ReportContextInput) { return post("/api/report/draft", input); } -export function reportFile(input: { actionType: ReportActionType; report: unknown; endorseIssueNumber?: number; endorseDiscussionId?: string; activityTrace?: string[]; screenshot?: ReportScreenshot }) { return post("/api/report/file", input); } +export function reportFile(input: { actionType: ReportActionType; report: unknown; endorseIssueNumber?: number; endorseDiscussionId?: string; endorseRoadmapIssueNumber?: number; activityTrace?: string[]; screenshot?: ReportScreenshot }) { return post("/api/report/file", input); } export function reportHelp(question: string) { return post("/api/report/help", { question }); } diff --git a/packages/dashboard/app/components/ReportModal.tsx b/packages/dashboard/app/components/ReportModal.tsx index 29f8e4e9b7..fec9bcc317 100644 --- a/packages/dashboard/app/components/ReportModal.tsx +++ b/packages/dashboard/app/components/ReportModal.tsx @@ -8,7 +8,7 @@ import "./ReportModal.css"; const prompts: Record = { bug: "What went wrong?", feedback: "What would you like to share?", idea: "What would you like Fusion to do?", help: "What would you like help with?" }; -type ModalResult = { kind: string; report?: { userPrompt: string; sourcePrompt?: string; summary?: string; body?: string; context?: Record; sessionToken?: string }; issue?: { number: number; url: string; title: string; discussionId?: string }; roadmap?: { featureId: string; title: string; description: string }; url?: string; answer?: { summary?: string; content?: string }; message?: string; screenshotNotAttached?: boolean }; +type ModalResult = { kind: string; report?: { userPrompt: string; sourcePrompt?: string; summary?: string; body?: string; context?: Record; sessionToken?: string }; issue?: { number: number; url: string; title: string; discussionId?: string; roadmap?: true }; url?: string; answer?: { summary?: string; content?: string }; message?: string; screenshotNotAttached?: boolean }; /** @@ -55,13 +55,13 @@ export function ReportModal({ actionType, onClose, contextRefs }: { actionType: setError("We could not prepare your report. Check your connection and try again."); } finally { setBusy(false); } }; - const file = async (endorseIssueNumber?: number, endorseDiscussionId?: string) => { + const file = async (endorseIssueNumber?: number, endorseDiscussionId?: string, endorseRoadmapIssueNumber?: number) => { if (!result?.report) return; setBusy(true); setError(undefined); try { recordActivity("report"); -setResult(await reportFile({ actionType, report: result.report, endorseIssueNumber, endorseDiscussionId, activityTrace: getRecentActivity(), screenshot: screenshotEnabled ? capturedScreenshot : undefined })); +setResult(await reportFile({ actionType, report: result.report, endorseIssueNumber, endorseDiscussionId, endorseRoadmapIssueNumber, activityTrace: getRecentActivity(), screenshot: screenshotEnabled ? capturedScreenshot : undefined })); } catch { setError("We could not send your report. Your draft is still here; try again."); @@ -96,7 +96,8 @@ setResult(await reportFile({ actionType, report: result.report, endorseIssueNumb structured data point and require an explicit confirmation after edits instead of posting a dedupe match immediately. */} -

Review data point for a similar open issue

+ {/* FNXC:ReportPipeline 2026-07-18-20:45: A public-roadmap duplicate is endorsed through the same reviewed data-point UI as an issue, so reporters strengthen the tracked item rather than opening a parallel thread. */} +

{result.issue.roadmap ? t("report.roadmapDuplicate.title", "Already on the roadmap — add your data point?") : "Review data point for a similar open issue"}

{result.issue.title}