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>
65 lines
2.1 KiB
TypeScript
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 [];
|
|
}
|
|
};
|
|
}
|