Files
fusion/packages/dashboard/src/report-roadmap-source.ts
gsxdsm 1215c76d9a FN-8326: add roadmap report deduplication
Add an optional public-roadmap source for report deduplication.

- Add project settings and localized controls for roadmap matching.
- Resolve roadmap entries through the plugin API and stop strong duplicate reports before filing.
- Cover report pipeline, route, UI, and roadmap store behavior with tests.

Files changed:
 .changeset/fn-8326-roadmap-report-dedup.md         |  7 +++
 docs/dashboard-guide.md                            |  2 +
 docs/settings-reference.md                         |  1 +
 .../core/src/__tests__/settings-parity.test.ts     |  3 +
 packages/core/src/settings-schema.ts               |  1 +
 packages/core/src/types.ts                         |  6 ++
 packages/dashboard/app/components/ReportModal.tsx  | 11 +++-
 .../app/components/__tests__/ReportModal.test.tsx  | 14 +++++
 .../__tests__/SettingsModal.general.test.tsx       |  2 +
 .../settings/sections/GeneralSection.tsx           | 10 ++++
 .../settings-default-descriptions.test.tsx         |  1 +
 .../src/__tests__/report-pipeline.test.ts          | 31 +++++++++++
 .../src/__tests__/report-roadmap-source.test.ts    | 32 +++++++++++
 .../dashboard/src/__tests__/report-routes.test.ts  | 27 +++++++--
 packages/dashboard/src/index.ts                    |  1 +
 packages/dashboard/src/report-pipeline.ts          | 36 +++++++++++-
 packages/dashboard/src/report-roadmap-source.ts    | 64 ++++++++++++++++++++++
 .../dashboard/src/routes/register-report-routes.ts |  3 +
 packages/i18n/locales/en/app.json                  | 10 +++-
 packages/i18n/locales/es/app.json                  | 10 +++-
 packages/i18n/locales/fr/app.json                  | 10 +++-
 packages/i18n/locales/ko/app.json                  | 10 +++-
 packages/i18n/locales/zh-CN/app.json               | 10 +++-
 packages/i18n/locales/zh-TW/app.json               | 10 +++-
 .../src/__tests__/roadmap-store-factory.test.ts    | 25 +++++++++
 .../src/routes/roadmap-routes.ts                   | 24 +-------
 plugins/fusion-plugin-roadmap/src/server/index.ts  | 34 ++++++++++++
 27 files changed, 360 insertions(+), 35 deletions(-)

Fusion-Task-Id: FN-8326

Fusion-Task-Lineage: e9731c3f-dd92-44ac-910b-0e0efbaaf288

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-18 14:23:14 -07:00

65 lines
2.1 KiB
TypeScript

import { createRoadmapStoreForTaskStore, type RoadmapTaskStoreAccess } from "@fusion-plugin-examples/roadmap";
import { getOrCreateProjectStore } from "./project-store-resolver.js";
export interface RoadmapCandidate {
featureId: string;
title: string;
body: string | null;
}
export type RoadmapDedupSource = (keywords: string[]) => Promise<RoadmapCandidate[]>;
type RoadmapHierarchyStore = {
listRoadmaps(): Promise<Array<{ id: string }>>;
getRoadmapWithHierarchy(id: string): Promise<{
milestones: Array<{
features: Array<{ id: string; title: string; description?: string }>;
}>;
} | undefined>;
};
export interface RoadmapDedupSourceDeps {
createRoadmapStore?: (taskStore: RoadmapTaskStoreAccess) => RoadmapHierarchyStore;
}
/**
* FNXC:ReportPipeline 2026-07-18-12:15:
* The roadmap is a read-only report deduplication corpus. A candidate match
* only informs pipeline routing and never mutates roadmap data or egresses it.
*/
export function createRoadmapDedupSourceForTaskStore(
taskStore: RoadmapTaskStoreAccess,
deps: RoadmapDedupSourceDeps = {},
): RoadmapDedupSource {
const createStore = deps.createRoadmapStore ?? createRoadmapStoreForTaskStore;
return async () => {
try {
const store = createStore(taskStore);
const roadmaps = await store.listRoadmaps();
const hierarchies = await Promise.all(roadmaps.map(({ id }) => store.getRoadmapWithHierarchy(id)));
return hierarchies.flatMap((roadmap) => roadmap?.milestones.flatMap((milestone) => milestone.features.map((feature) => ({
featureId: feature.id,
title: feature.title,
body: feature.description ?? null,
}))) ?? []);
} catch {
// The optional roadmap plugin and its PostgreSQL layer must not block reports.
return [];
}
};
}
export function createProjectRoadmapDedupSource(
projectId: string,
deps: RoadmapDedupSourceDeps = {},
): RoadmapDedupSource {
return async (keywords) => {
try {
const taskStore = await getOrCreateProjectStore(projectId);
return await createRoadmapDedupSourceForTaskStore(taskStore, deps)(keywords);
} catch {
return [];
}
};
}