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) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-8310-public-roadmap-dedupe.md
Normal file
7
.changeset/fn-8310-public-roadmap-dedupe.md
Normal file
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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<Record<"bug" \| "feedback" \| "idea" \| "help", "draft-review" \| "auto-file">>` | `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). |
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<Record<ReportActionType, ReportMode>>;
|
||||
/**
|
||||
* 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.
|
||||
|
||||
@@ -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 }); }
|
||||
|
||||
@@ -8,7 +8,7 @@ import "./ReportModal.css";
|
||||
|
||||
const prompts: Record<ReportActionType, string> = { 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<string, unknown>; 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<string, unknown>; 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.
|
||||
*/}
|
||||
<h2>Review data point for a similar open issue</h2>
|
||||
{/* 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. */}
|
||||
<h2>{result.issue.roadmap ? t("report.roadmapDuplicate.title", "Already on the roadmap — add your data point?") : "Review data point for a similar open issue"}</h2>
|
||||
<a href={result.issue.url} target="_blank" rel="noreferrer">{result.issue.title}</a>
|
||||
<label htmlFor="report-duplicate-prompt">Report summary</label>
|
||||
<textarea id="report-duplicate-prompt" className="input" value={result.report.userPrompt} onChange={(event) => {
|
||||
@@ -105,16 +106,9 @@ setResult(await reportFile({ actionType, report: result.report, endorseIssueNumb
|
||||
}} />
|
||||
<label htmlFor="report-duplicate-body">Structured data point</label>
|
||||
<textarea id="report-duplicate-body" className="input" value={result.report.body ?? ""} onChange={(event) => setResult({ ...result, report: { ...result.report!, body: event.target.value } })} />
|
||||
<button className="btn btn-primary" type="button" disabled={busy} onClick={() => void file(result.issue!.discussionId ? undefined : result.issue!.number, result.issue!.discussionId)}>Confirm and add data point</button>
|
||||
<button className="btn btn-primary" type="button" disabled={busy} onClick={() => void file(result.issue!.discussionId ? undefined : result.issue!.roadmap ? undefined : result.issue!.number, result.issue!.discussionId, result.issue!.roadmap ? result.issue!.number : undefined)}>Confirm and add data point</button>
|
||||
</>}
|
||||
|
||||
{result?.kind === "roadmap-match" && result.roadmap && <>
|
||||
{/* FNXC:ReportPipeline 2026-07-18-12:45: A roadmap match stays inline because the roadmap view is intentionally not a dashboard destination; it informs the reporter and offers no dead deep-link or filing control. */}
|
||||
<h2>{t("report.roadmapMatch.title", "Already on the roadmap")}</h2>
|
||||
<p>{t("report.roadmapMatch.message", "This report matches a feature that is already planned.")}</p>
|
||||
<h3>{result.roadmap.title}</h3>
|
||||
{result.roadmap.description && <p>{result.roadmap.description}</p>}
|
||||
</>}
|
||||
{(result?.kind === "filed" || result?.kind === "endorsed") && <><h2>{result.screenshotNotAttached ? "Report sent without screenshot" : "Report sent"}</h2>{result.screenshotNotAttached && <p className="report-modal__warning" role="status">Your report was sent, but the screenshot could not be attached. No screenshot pixels were shared.</p>}<a href={result.url} target="_blank" rel="noreferrer">View on GitHub</a>{result.report?.body && <><label htmlFor="filed-report">Final report</label><textarea id="filed-report" className="input" value={result.report.body} readOnly /></>}</>}
|
||||
|
||||
{result?.kind === "help" && <><h2>Suggested help</h2><p>{result.answer?.summary ?? result.answer?.content}</p></>}
|
||||
|
||||
@@ -811,7 +811,7 @@ type PluginsSubsectionId = "fusion-plugins" | "pi-extensions";
|
||||
|
||||
/** Local form state extends Settings with a worktreeInitCommand override and lets tokenCap carry null (delete semantic). */
|
||||
type SettingsFormState = Settings & { worktreeInitCommand?: string; tokenCap?: number | null };
|
||||
type GlobalGitlabSettings = Pick<GlobalSettings, "gitlabEnabled" | "gitlabInstanceUrl" | "gitlabApiBaseUrl" | "gitlabAuthToken" | "gitlabAuthTokenType">;
|
||||
type GlobalSourceControlSettings = Pick<GlobalSettings, "gitlabEnabled" | "gitlabInstanceUrl" | "gitlabApiBaseUrl" | "gitlabAuthToken" | "gitlabAuthTokenType" | "reportRoadmapDedupeEnabled" | "reportRoadmapLabel" | "reportRoadmapRepo">;
|
||||
|
||||
interface SettingsModalProps {
|
||||
onClose: () => void;
|
||||
@@ -1208,7 +1208,7 @@ export function SettingsModal({
|
||||
// Track scoped settings for inheritance detection (fetched alongside merged settings)
|
||||
// This stores the raw { global, project } structure from the API
|
||||
const [scopedSettings, setScopedSettings] = useState<{ global: GlobalSettings; project: Partial<Settings> } | null>(null);
|
||||
const [globalGitlabSettings, setGlobalGitlabSettings] = useState<GlobalGitlabSettings | null>(null);
|
||||
const [globalGitlabSettings, setGlobalGitlabSettings] = useState<GlobalSourceControlSettings | null>(null);
|
||||
// Track initial scoped values for null-as-delete semantics on project overrides
|
||||
const [initialScopedValues, setInitialScopedValues] = useState<{ global: GlobalSettings; project: Partial<Settings> } | null>(null);
|
||||
const mcpFormForScope = useCallback((scope: McpSettingsScope): Settings => ({
|
||||
@@ -1768,6 +1768,9 @@ export function SettingsModal({
|
||||
gitlabApiBaseUrl: scoped.global.gitlabApiBaseUrl,
|
||||
gitlabAuthToken: scoped.global.gitlabAuthToken,
|
||||
gitlabAuthTokenType: scoped.global.gitlabAuthTokenType,
|
||||
reportRoadmapDedupeEnabled: scoped.global.reportRoadmapDedupeEnabled,
|
||||
reportRoadmapLabel: scoped.global.reportRoadmapLabel,
|
||||
reportRoadmapRepo: scoped.global.reportRoadmapRepo,
|
||||
});
|
||||
setInitialScopedValues({
|
||||
...scoped,
|
||||
@@ -3484,6 +3487,9 @@ export function SettingsModal({
|
||||
gitlabApiBaseUrl: gitlabFormForSave.gitlabApiBaseUrl?.trim() || undefined,
|
||||
gitlabAuthToken: gitlabFormForSave.gitlabAuthToken?.trim() || undefined,
|
||||
gitlabAuthTokenType: gitlabFormForSave.gitlabAuthTokenType ?? "personal",
|
||||
reportRoadmapDedupeEnabled: gitlabFormForSave.reportRoadmapDedupeEnabled,
|
||||
reportRoadmapLabel: gitlabFormForSave.reportRoadmapLabel?.trim() || undefined,
|
||||
reportRoadmapRepo: gitlabFormForSave.reportRoadmapRepo?.trim() || undefined,
|
||||
githubAuthToken: form.githubAuthToken?.trim() || undefined,
|
||||
prTitlePromptInstructions: form.prTitlePromptInstructions?.trim() || undefined,
|
||||
prDescriptionPromptInstructions: form.prDescriptionPromptInstructions?.trim() || undefined,
|
||||
@@ -3859,12 +3865,15 @@ export function SettingsModal({
|
||||
form={form}
|
||||
setForm={setForm}
|
||||
globalSettings={globalGitlabSettings}
|
||||
onGlobalGitlabSettingsChange={(patch) => setGlobalGitlabSettings((current) => ({
|
||||
onGlobalSourceControlSettingsChange={(patch) => setGlobalGitlabSettings((current) => ({
|
||||
gitlabEnabled: current?.gitlabEnabled,
|
||||
gitlabInstanceUrl: current?.gitlabInstanceUrl,
|
||||
gitlabApiBaseUrl: current?.gitlabApiBaseUrl,
|
||||
gitlabAuthToken: current?.gitlabAuthToken,
|
||||
gitlabAuthTokenType: current?.gitlabAuthTokenType,
|
||||
reportRoadmapDedupeEnabled: current?.reportRoadmapDedupeEnabled,
|
||||
reportRoadmapLabel: current?.reportRoadmapLabel,
|
||||
reportRoadmapRepo: current?.reportRoadmapRepo,
|
||||
...patch,
|
||||
}))}
|
||||
globalTrackingRepoOptions={globalTrackingRepoOptions}
|
||||
|
||||
@@ -62,18 +62,16 @@ describe("ReportModal", () => {
|
||||
})));
|
||||
});
|
||||
|
||||
it("shows a roadmap match inline without a filing or dead-link affordance", async () => {
|
||||
reportDraft.mockResolvedValueOnce({ kind: "roadmap-match", roadmap: { featureId: "RF-1", title: "Offline report queue", description: "Keep reports available while offline" } });
|
||||
it("offers a reviewed endorsement for a roadmap issue", async () => {
|
||||
reportDraft.mockResolvedValueOnce({ kind: "duplicate-found", report: { userPrompt: "Keep reports while offline", body: "data" }, issue: { number: 30, title: "Offline report queue", url: "https://example.test/issues/30", roadmap: true } });
|
||||
render(<ReportModal actionType="idea" onClose={vi.fn()} />);
|
||||
|
||||
fireEvent.change(screen.getByLabelText("What would you like Fusion to do?"), { target: { value: "Keep reports while offline" } });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Continue" }));
|
||||
|
||||
expect(await screen.findByText("Already on the roadmap")).toBeInTheDocument();
|
||||
expect(screen.getByText("Offline report queue")).toBeInTheDocument();
|
||||
expect(screen.getByText("Keep reports available while offline")).toBeInTheDocument();
|
||||
expect(screen.queryByRole("link")).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: /file/i })).not.toBeInTheDocument();
|
||||
expect(await screen.findByText("Already on the roadmap — add your data point?")).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Confirm and add data point" }));
|
||||
await waitFor(() => expect(reportFile).toHaveBeenCalledWith(expect.objectContaining({ endorseRoadmapIssueNumber: 30 })));
|
||||
});
|
||||
|
||||
it("lets people return to the guided prompt after an unavailable response", async () => {
|
||||
|
||||
@@ -1336,14 +1336,16 @@ describe("SettingsModal", () => {
|
||||
|
||||
fireEvent.change(screen.getByLabelText("In-app report mode"), { target: { value: "auto-file" } });
|
||||
fireEvent.change(screen.getByLabelText("Bug report override"), { target: { value: "draft-review" } });
|
||||
fireEvent.click(screen.getByLabelText("Check roadmap before filing reports"));
|
||||
fireEvent.click(screen.getByLabelText("Deduplicate reports against public roadmap"));
|
||||
fireEvent.change(screen.getByLabelText("Public roadmap label"), { target: { value: "planned" } });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Save" }));
|
||||
|
||||
await waitFor(() => expect(mockUpdateSettings).toHaveBeenCalled());
|
||||
expect(mockUpdateSettings.mock.calls[0]?.[0]).toEqual(expect.objectContaining({
|
||||
reportMode: "auto-file",
|
||||
reportModeByAction: { bug: "draft-review" },
|
||||
reportRoadmapDedup: true,
|
||||
reportRoadmapDedupeEnabled: false,
|
||||
reportRoadmapLabel: "planned",
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
@@ -199,7 +199,7 @@ describe("settings section-keys registry", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("source-control-global owns the global GitLab fallbacks and the global tracking repo", () => {
|
||||
it("source-control-global owns global source-control fallbacks", () => {
|
||||
const entry = getSectionKeyEntry("source-control-global")!;
|
||||
expect(entry.scope).toBe("global");
|
||||
expect(new Set(entry.keys)).toEqual(
|
||||
@@ -210,6 +210,9 @@ describe("settings section-keys registry", () => {
|
||||
"gitlabApiBaseUrl",
|
||||
"gitlabAuthToken",
|
||||
"gitlabAuthTokenType",
|
||||
"reportRoadmapDedupeEnabled",
|
||||
"reportRoadmapLabel",
|
||||
"reportRoadmapRepo",
|
||||
]),
|
||||
);
|
||||
|
||||
|
||||
@@ -72,14 +72,14 @@ const MODEL_LANE_KEY_SET = new Set<string>(MODEL_LANE_KEYS);
|
||||
|
||||
/*
|
||||
FNXC:GitLabEnablement 2026-07-04-00:00:
|
||||
FN-7535: the five global GitLab keys must be diffed against the SCOPED global
|
||||
FN-7535: the global GitLab and public-roadmap fallback keys must be diffed against the SCOPED global
|
||||
initial only — never the merged, project-effective `initialValues` — because
|
||||
SettingsModal already edits these keys through a dedicated `globalGitlabSettings`
|
||||
state seeded from `scoped.global` (FN-7453). Falling back to merged initialValues
|
||||
when the scoped global object lacks the key (e.g. the operator has never saved a
|
||||
global value before) let a project override's effective value silently stand in
|
||||
for "no change", so a genuine global edit that happened to match the merged value
|
||||
was dropped from the global patch. These keys never fall back to `initialValues`.
|
||||
was dropped from the global patch. These scoped-only keys never fall back to `initialValues`.
|
||||
*/
|
||||
const GLOBAL_GITLAB_SCOPED_ONLY_KEYS = new Set<string>([
|
||||
"gitlabEnabled",
|
||||
@@ -87,6 +87,9 @@ const GLOBAL_GITLAB_SCOPED_ONLY_KEYS = new Set<string>([
|
||||
"gitlabApiBaseUrl",
|
||||
"gitlabAuthToken",
|
||||
"gitlabAuthTokenType",
|
||||
"reportRoadmapDedupeEnabled",
|
||||
"reportRoadmapLabel",
|
||||
"reportRoadmapRepo",
|
||||
]);
|
||||
|
||||
type RemoteAccessProvider = "tailscale" | "cloudflare";
|
||||
@@ -137,6 +140,9 @@ export const GLOBAL_SECTION_KEYS: Record<string, ReadonlySet<string>> = {
|
||||
"gitlabApiBaseUrl",
|
||||
"gitlabAuthToken",
|
||||
"gitlabAuthTokenType",
|
||||
"reportRoadmapDedupeEnabled",
|
||||
"reportRoadmapLabel",
|
||||
"reportRoadmapRepo",
|
||||
]),
|
||||
"global-general": new Set([
|
||||
"language",
|
||||
@@ -503,10 +509,10 @@ export function splitSettingsSave({
|
||||
if (key === "githubTokenConfigured" || key === "prAuthAvailable") continue; // server-only
|
||||
if (key === "customProviders") continue; // persisted via dedicated routes, not save-split (see global branch above)
|
||||
// Mirror of the global branch's dual-scope gate: while the global source-control
|
||||
// section is active these six keys are the GLOBAL fallbacks, so they must not
|
||||
// section is active these source-control keys are the GLOBAL fallbacks, so they must not
|
||||
// also be written as project overrides. See the FNXC note in the global branch.
|
||||
if (key === "githubTrackingDefaultRepo" && activeSection === "source-control-global") continue;
|
||||
if ((key === "gitlabEnabled" || key === "gitlabInstanceUrl" || key === "gitlabApiBaseUrl" || key === "gitlabAuthToken" || key === "gitlabAuthTokenType") && activeSection === "source-control-global") continue;
|
||||
if ((key === "gitlabEnabled" || key === "gitlabInstanceUrl" || key === "gitlabApiBaseUrl" || key === "gitlabAuthToken" || key === "gitlabAuthTokenType" || key === "reportRoadmapDedupeEnabled" || key === "reportRoadmapLabel" || key === "reportRoadmapRepo") && activeSection === "source-control-global") continue;
|
||||
if (key === "mcpServers" && scopedMcpValues) continue;
|
||||
if (key === "mcpServers" && activeSection === "global-mcp") continue;
|
||||
if (!isProjectSettingsKey(key)) continue;
|
||||
|
||||
@@ -78,6 +78,11 @@ const PROJECT_SECTION_KEYS: Record<string, readonly string[]> = {
|
||||
"showTaskChatsInCommonFeed",
|
||||
"taskPrefix",
|
||||
"workspaceMode",
|
||||
"reportMode",
|
||||
"reportModeByAction",
|
||||
"reportRoadmapDedupeEnabled",
|
||||
"reportRoadmapLabel",
|
||||
"reportRoadmapRepo",
|
||||
],
|
||||
/*
|
||||
FNXC:SourceControl 2026-07-15-20:30:
|
||||
|
||||
@@ -203,18 +203,32 @@ export const generalSearchEntries: SettingsSearchEntry[] = [
|
||||
"Language imported issues are translated into when auto-translation is enabled. No default — unset inherits the dashboard language.",
|
||||
keywords: ["locale", "localization"],
|
||||
},
|
||||
/*
|
||||
FNXC:ReportPipeline 2026-07-18-14:45:
|
||||
FN-8326 surfaces reportRoadmapDedup as a SettingsToggleRow in GeneralSection; index it so operators can find roadmap-dedup before filing.
|
||||
*/
|
||||
/* FNXC:ReportPipeline 2026-07-18-20:45: FR-30 replaces the removed local roadmap toggle with all project-level public-roadmap controls, so search reaches the visible controls and their current defaults. */
|
||||
{
|
||||
sectionId: "general",
|
||||
key: "reportRoadmapDedup",
|
||||
labelKey: "settings.general.reportRoadmapDedup",
|
||||
labelFallback: "Check roadmap before filing reports",
|
||||
helpKey: "settings.general.reportRoadmapDedupHelp",
|
||||
helpFallback:
|
||||
"When enabled, matching roadmap features are shown inline instead of filing another GitHub Issue or Discussion. Default: off.",
|
||||
key: "reportRoadmapDedupeEnabled",
|
||||
labelKey: "settings.general.reportRoadmapDedupeEnabled",
|
||||
labelFallback: "Deduplicate reports against public roadmap",
|
||||
helpKey: "settings.general.reportRoadmapDedupeEnabledHelp",
|
||||
helpFallback: "Match open, labeled roadmap issues before filing. Default: on.",
|
||||
keywords: ["roadmap", "dedup", "report", "duplicate"],
|
||||
},
|
||||
{
|
||||
sectionId: "general",
|
||||
key: "reportRoadmapLabel",
|
||||
labelKey: "settings.general.reportRoadmapLabel",
|
||||
labelFallback: "Public roadmap label",
|
||||
helpKey: "settings.general.reportRoadmapLabelHelp",
|
||||
helpFallback: "Open GitHub Issues with this label are considered roadmap items. Default: roadmap.",
|
||||
keywords: ["roadmap", "label", "report", "duplicate"],
|
||||
},
|
||||
{
|
||||
sectionId: "general",
|
||||
key: "reportRoadmapRepo",
|
||||
labelKey: "settings.general.reportRoadmapRepo",
|
||||
labelFallback: "Public roadmap repository (optional)",
|
||||
helpKey: "settings.general.reportRoadmapRepoHelp",
|
||||
helpFallback: "GitHub owner/repository containing public roadmap issues. When unset, uses the tracking repository. No default — unset.",
|
||||
keywords: ["roadmap", "repository", "repo", "tracking"],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -336,13 +336,32 @@ export function GeneralSection({ form, setForm, projectId, addToast, prefixError
|
||||
))}
|
||||
<SettingsToggleRow
|
||||
descriptor={{
|
||||
key: "reportRoadmapDedup",
|
||||
label: t("settings.general.reportRoadmapDedup", "Check roadmap before filing reports"),
|
||||
help: t("settings.general.reportRoadmapDedupHelp", "When enabled, matching roadmap features are shown inline instead of filing another GitHub Issue or Discussion. Default: off."),
|
||||
key: "reportRoadmapDedupeEnabled",
|
||||
label: t("settings.general.reportRoadmapDedupeEnabled", "Deduplicate reports against public roadmap"),
|
||||
help: t("settings.general.reportRoadmapDedupeEnabledHelp", "Match open, labeled roadmap issues before filing. Default: on."),
|
||||
scope: "project",
|
||||
}}
|
||||
value={form.reportRoadmapDedup === true}
|
||||
onChange={(value) => setForm((current) => ({ ...current, reportRoadmapDedup: value ?? false }))}
|
||||
value={form.reportRoadmapDedupeEnabled ?? true}
|
||||
onChange={(value) => setForm((current) => ({ ...current, reportRoadmapDedupeEnabled: value ?? true }))}
|
||||
/>
|
||||
<SettingsTextRow
|
||||
descriptor={{
|
||||
key: "reportRoadmapLabel",
|
||||
label: t("settings.general.reportRoadmapLabel", "Public roadmap label"),
|
||||
help: t("settings.general.reportRoadmapLabelHelp", "Open GitHub Issues with this label are considered roadmap items. Default: roadmap."),
|
||||
}}
|
||||
value={form.reportRoadmapLabel ?? "roadmap"}
|
||||
onChange={(value) => setForm((current) => ({ ...current, reportRoadmapLabel: value || undefined }))}
|
||||
/>
|
||||
<SettingsTextRow
|
||||
descriptor={{
|
||||
key: "reportRoadmapRepo",
|
||||
label: t("settings.general.reportRoadmapRepo", "Public roadmap repository (optional)"),
|
||||
help: t("settings.general.reportRoadmapRepoHelp", "GitHub owner/repository containing public roadmap issues. When unset, uses the tracking repository. No default — unset."),
|
||||
placeholder: "owner/repository",
|
||||
}}
|
||||
value={form.reportRoadmapRepo ?? ""}
|
||||
onChange={(value) => setForm((current) => ({ ...current, reportRoadmapRepo: value || undefined }))}
|
||||
/>
|
||||
</div>
|
||||
{/*
|
||||
|
||||
@@ -9,6 +9,33 @@
|
||||
import type { SettingsSearchEntry } from "../search/types";
|
||||
|
||||
export const sourceControlGlobalSearchEntries: SettingsSearchEntry[] = [
|
||||
{
|
||||
sectionId: "source-control-global",
|
||||
key: "reportRoadmapDedupeEnabled",
|
||||
labelKey: "settings.globalGeneral.reportRoadmapDedupeEnabled",
|
||||
labelFallback: "Global public-roadmap deduplication",
|
||||
helpKey: "settings.globalGeneral.reportRoadmapDedupeEnabledHelp",
|
||||
helpFallback: "Fallback for projects that do not set a public-roadmap deduplication preference. No default — unset (projects default to enabled).",
|
||||
keywords: ["roadmap", "dedup", "report", "fallback"],
|
||||
},
|
||||
{
|
||||
sectionId: "source-control-global",
|
||||
key: "reportRoadmapLabel",
|
||||
labelKey: "settings.globalGeneral.reportRoadmapLabel",
|
||||
labelFallback: "Global public roadmap label",
|
||||
helpKey: "settings.globalGeneral.reportRoadmapLabelHelp",
|
||||
helpFallback: "Fallback label for open public-roadmap issues when a project does not set one. No default — unset (projects default to roadmap).",
|
||||
keywords: ["roadmap", "label", "report", "fallback"],
|
||||
},
|
||||
{
|
||||
sectionId: "source-control-global",
|
||||
key: "reportRoadmapRepo",
|
||||
labelKey: "settings.globalGeneral.reportRoadmapRepo",
|
||||
labelFallback: "Global public roadmap repository (optional)",
|
||||
helpKey: "settings.globalGeneral.reportRoadmapRepoHelp",
|
||||
helpFallback: "Fallback GitHub owner/repository for public-roadmap issues. When unset, uses the tracking repository. No default — unset.",
|
||||
keywords: ["roadmap", "repository", "repo", "tracking", "fallback"],
|
||||
},
|
||||
{
|
||||
sectionId: "source-control-global",
|
||||
key: "gitlabInstanceUrl",
|
||||
|
||||
@@ -2,15 +2,16 @@ import type { GlobalSettings } from "@fusion/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TrackingRepoSelect, type TrackingRepoOption } from "../../TrackingRepoSelect";
|
||||
import { SettingsSelectRow } from "../SettingsSelectRow";
|
||||
import { SettingsToggleRow } from "../SettingsToggleRow";
|
||||
import { SettingsTextRow } from "../SettingsTextRow";
|
||||
import { SettingsHelpTip } from "../SettingsHelpTip";
|
||||
import type { SectionBaseProps } from "./context";
|
||||
|
||||
type GlobalGitlabSettings = Pick<GlobalSettings, "gitlabEnabled" | "gitlabInstanceUrl" | "gitlabApiBaseUrl" | "gitlabAuthToken" | "gitlabAuthTokenType">;
|
||||
type GlobalSourceControlSettings = Pick<GlobalSettings, "gitlabEnabled" | "gitlabInstanceUrl" | "gitlabApiBaseUrl" | "gitlabAuthToken" | "gitlabAuthTokenType" | "reportRoadmapDedupeEnabled" | "reportRoadmapLabel" | "reportRoadmapRepo">;
|
||||
|
||||
export interface SourceControlGlobalSectionProps extends SectionBaseProps {
|
||||
globalSettings: GlobalGitlabSettings | null;
|
||||
onGlobalGitlabSettingsChange: (patch: Partial<GlobalGitlabSettings>) => void;
|
||||
globalSettings: GlobalSourceControlSettings | null;
|
||||
onGlobalSourceControlSettingsChange: (patch: Partial<GlobalSourceControlSettings>) => void;
|
||||
globalTrackingRepoOptions: TrackingRepoOption[];
|
||||
globalTrackingRepoLoading: boolean;
|
||||
globalTrackingRepoError: string | null;
|
||||
@@ -19,21 +20,22 @@ export interface SourceControlGlobalSectionProps extends SectionBaseProps {
|
||||
/*
|
||||
FNXC:SourceControl 2026-07-15-20:30:
|
||||
The global GitLab fallbacks and the global default tracking repo moved out of "General · Global" into their own section, adjacent to "Source Control · Project" under the Integrations nav group. They are integrations with GitHub/GitLab, not general app preferences, and pairing the two scopes is what lets an operator see a global fallback and its project override without hunting across unrelated sections.
|
||||
NOTE for future edits: `splitSettingsSave` (save-split.ts) gates these six dual-scope keys on the ACTIVE SECTION ID — they route to the global patch only while this section is open, and to the project patch everywhere else. That guard names `source-control-global` literally. Renaming this section id without updating save-split.ts would silently write these global fallbacks into project settings.
|
||||
NOTE for future edits: `splitSettingsSave` (save-split.ts) gates these nine dual-scope keys on the ACTIVE SECTION ID — they route to the global patch only while this section is open, and to the project patch everywhere else. That guard names `source-control-global` literally. Renaming this section id without updating save-split.ts would silently write these global fallbacks into project settings.
|
||||
|
||||
FNXC:SettingsScope 2026-07-15-20:30:
|
||||
No scope badge on any row here: all six keys (`gitlabEnabled`, `gitlabInstanceUrl`, `gitlabApiBaseUrl`, `gitlabAuthTokenType`, `gitlabAuthToken`, `githubTrackingDefaultRepo`) are declared in BOTH `DEFAULT_GLOBAL_SETTINGS` and `DEFAULT_PROJECT_SETTINGS`, so no badge can state their scope honestly. The section name ("Source Control · Global") carries it, as does each label's "Global …" prefix.
|
||||
No scope badge on any row here: all nine keys (`gitlabEnabled`, `gitlabInstanceUrl`, `gitlabApiBaseUrl`, `gitlabAuthTokenType`, `gitlabAuthToken`, `githubTrackingDefaultRepo`, `reportRoadmapDedupeEnabled`, `reportRoadmapLabel`, `reportRoadmapRepo`) are declared in BOTH `DEFAULT_GLOBAL_SETTINGS` and `DEFAULT_PROJECT_SETTINGS`, so no badge can state their scope honestly. The section name ("Source Control · Global") carries it, as does each label's "Global …" prefix.
|
||||
|
||||
FNXC:SettingsStyling 2026-07-15-20:30:
|
||||
Plain label+control+help rows use the shared primitives; `gitlabAuthToken` uses the primitive's `type: "password"` (defaulting `autocomplete="off"`). The tracking-repo select and the disclosure chrome stay bespoke — they are custom widgets, not label+control+help rows.
|
||||
*/
|
||||
export function SourceControlGlobalSection({ form, setForm, globalSettings, onGlobalGitlabSettingsChange, globalTrackingRepoOptions, globalTrackingRepoLoading, globalTrackingRepoError, }: SourceControlGlobalSectionProps) {
|
||||
export function SourceControlGlobalSection({ form, setForm, globalSettings, onGlobalSourceControlSettingsChange, globalTrackingRepoOptions, globalTrackingRepoLoading, globalTrackingRepoError, }: SourceControlGlobalSectionProps) {
|
||||
const { t } = useTranslation("app");
|
||||
/*
|
||||
FNXC:GitLabEnablement 2026-07-04-00:00:
|
||||
The GitLab rows read from the SCOPED global values (`globalSettings`), not the merged `form`, so a project override never renders as the global fallback's value. `form` is only the fallback while the scoped fetch is in flight.
|
||||
*/
|
||||
const globalGitlab = globalSettings ?? form;
|
||||
const globalSourceControl = globalSettings ?? form;
|
||||
const globalGitlab = globalSourceControl;
|
||||
return (<>
|
||||
{/* FNXC:SettingsHelp 2026-07-16-12:45: Inline help moved behind the shared "?" affordance beside the label — operator requirement: no inline description paragraphs in Settings. The bespoke TrackingRepoSelect widget is no obstacle: the tip belongs to the label line, not the control. */}
|
||||
<div className="form-group">
|
||||
@@ -43,6 +45,35 @@ export function SourceControlGlobalSection({ form, setForm, globalSettings, onGl
|
||||
</div>
|
||||
<TrackingRepoSelect id="globalGithubTrackingDefaultRepo" ariaLabel="Global default tracking repo" value={form.githubTrackingDefaultRepo ?? ""} options={globalTrackingRepoOptions} loading={globalTrackingRepoLoading} error={globalTrackingRepoError ?? undefined} placeholder={t("settings.globalGeneral.ownerRepo", "owner/repo")} onChange={(nextValue) => setForm((f) => ({ ...f, githubTrackingDefaultRepo: nextValue || undefined }))}/>
|
||||
</div>
|
||||
{/* FNXC:ReportPipeline 2026-07-18-20:45: FR-30 exposes global fallback values beside the global tracking repository so operators can configure a machine-wide public-roadmap policy without creating a project override. */}
|
||||
<SettingsToggleRow
|
||||
descriptor={{
|
||||
key: "reportRoadmapDedupeEnabled",
|
||||
label: t("settings.globalGeneral.reportRoadmapDedupeEnabled", "Global public-roadmap deduplication"),
|
||||
help: t("settings.globalGeneral.reportRoadmapDedupeEnabledHelp", "Fallback for projects that do not set a public-roadmap deduplication preference. No default — unset (projects default to enabled)."),
|
||||
}}
|
||||
value={globalSourceControl.reportRoadmapDedupeEnabled ?? true}
|
||||
onChange={(value) => onGlobalSourceControlSettingsChange({ reportRoadmapDedupeEnabled: value ?? undefined })}
|
||||
/>
|
||||
<SettingsTextRow
|
||||
descriptor={{
|
||||
key: "reportRoadmapLabel",
|
||||
label: t("settings.globalGeneral.reportRoadmapLabel", "Global public roadmap label"),
|
||||
help: t("settings.globalGeneral.reportRoadmapLabelHelp", "Fallback label for open public-roadmap issues when a project does not set one. No default — unset (projects default to roadmap)."),
|
||||
}}
|
||||
value={globalSourceControl.reportRoadmapLabel ?? ""}
|
||||
onChange={(value) => onGlobalSourceControlSettingsChange({ reportRoadmapLabel: value || undefined })}
|
||||
/>
|
||||
<SettingsTextRow
|
||||
descriptor={{
|
||||
key: "reportRoadmapRepo",
|
||||
label: t("settings.globalGeneral.reportRoadmapRepo", "Global public roadmap repository (optional)"),
|
||||
help: t("settings.globalGeneral.reportRoadmapRepoHelp", "Fallback GitHub owner/repository for public-roadmap issues. When unset, uses the tracking repository. No default — unset."),
|
||||
placeholder: "owner/repository",
|
||||
}}
|
||||
value={globalSourceControl.reportRoadmapRepo ?? ""}
|
||||
onChange={(value) => onGlobalSourceControlSettingsChange({ reportRoadmapRepo: value || undefined })}
|
||||
/>
|
||||
{/*
|
||||
FNXC:GitLabEnablement 2026-07-02-00:00:
|
||||
FN-7453 adds a global GitLab enable fallback that can disable outbound GitLab HTTP API operations without deleting saved self-managed URL or token settings. Projects can override the enabled state when they need GitLab active while the global fallback is off.
|
||||
@@ -51,7 +82,7 @@ export function SourceControlGlobalSection({ form, setForm, globalSettings, onGl
|
||||
<summary>
|
||||
<span className="settings-gitlab-disclosure__title">{t("settings.globalGeneral.gitLabConfiguration", "GitLab Configuration")}</span>
|
||||
<label className="checkbox-label settings-gitlab-disclosure__toggle" htmlFor="globalGitlabEnabled" onClick={(event) => event.stopPropagation()}>
|
||||
<input id="globalGitlabEnabled" type="checkbox" checked={globalGitlab.gitlabEnabled !== false} onChange={(e) => onGlobalGitlabSettingsChange({ gitlabEnabled: e.target.checked })}/>
|
||||
<input id="globalGitlabEnabled" type="checkbox" checked={globalGitlab.gitlabEnabled !== false} onChange={(e) => onGlobalSourceControlSettingsChange({ gitlabEnabled: e.target.checked })}/>
|
||||
{t("settings.globalGeneral.enableGitLabIntegration", "Enable GitLab integration")}
|
||||
</label>
|
||||
{/*
|
||||
@@ -74,7 +105,7 @@ export function SourceControlGlobalSection({ form, setForm, globalSettings, onGl
|
||||
disabled: globalGitlab.gitlabEnabled === false,
|
||||
}}
|
||||
value={globalGitlab.gitlabInstanceUrl ?? ""}
|
||||
onChange={(v) => onGlobalGitlabSettingsChange({ gitlabInstanceUrl: v || undefined })}
|
||||
onChange={(v) => onGlobalSourceControlSettingsChange({ gitlabInstanceUrl: v || undefined })}
|
||||
/>
|
||||
<SettingsTextRow
|
||||
descriptor={{
|
||||
@@ -86,7 +117,7 @@ export function SourceControlGlobalSection({ form, setForm, globalSettings, onGl
|
||||
disabled: globalGitlab.gitlabEnabled === false,
|
||||
}}
|
||||
value={globalGitlab.gitlabApiBaseUrl ?? ""}
|
||||
onChange={(v) => onGlobalGitlabSettingsChange({ gitlabApiBaseUrl: v || undefined })}
|
||||
onChange={(v) => onGlobalSourceControlSettingsChange({ gitlabApiBaseUrl: v || undefined })}
|
||||
/>
|
||||
<SettingsSelectRow
|
||||
descriptor={{
|
||||
@@ -101,7 +132,7 @@ export function SourceControlGlobalSection({ form, setForm, globalSettings, onGl
|
||||
],
|
||||
}}
|
||||
value={globalGitlab.gitlabAuthTokenType ?? "personal"}
|
||||
onChange={(v) => onGlobalGitlabSettingsChange({ gitlabAuthTokenType: v as "personal" | "project" | "group" })}
|
||||
onChange={(v) => onGlobalSourceControlSettingsChange({ gitlabAuthTokenType: v as "personal" | "project" | "group" })}
|
||||
/>
|
||||
<SettingsTextRow
|
||||
descriptor={{
|
||||
@@ -112,7 +143,7 @@ export function SourceControlGlobalSection({ form, setForm, globalSettings, onGl
|
||||
disabled: globalGitlab.gitlabEnabled === false,
|
||||
}}
|
||||
value={globalGitlab.gitlabAuthToken ?? ""}
|
||||
onChange={(v) => onGlobalGitlabSettingsChange({ gitlabAuthToken: v || undefined })}
|
||||
onChange={(v) => onGlobalSourceControlSettingsChange({ gitlabAuthToken: v || undefined })}
|
||||
/>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
@@ -269,7 +269,9 @@ const SETTING_DESCRIPTION_KEYS: Record<string, string> = {
|
||||
*/
|
||||
reportMode: "general.reportModeHelp",
|
||||
reportModeByAction: "general.reportModeByActionHelp",
|
||||
reportRoadmapDedup: "general.reportRoadmapDedupHelp",
|
||||
reportRoadmapDedupeEnabled: "globalGeneral.reportRoadmapDedupeEnabledHelp",
|
||||
reportRoadmapLabel: "globalGeneral.reportRoadmapLabelHelp",
|
||||
reportRoadmapRepo: "globalGeneral.reportRoadmapRepoHelp",
|
||||
githubTrackingDedupEnabled: "general.whenEnabledFusionChecksOpenAndClosedIssues",
|
||||
githubTrackingEnabledByDefault: "general.offDefault",
|
||||
sessionAdvisorEnabledByDefault: "general.offDefault",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { endorseDuplicate, runReportPipeline, type ReportPipelineDeps } from "../report-pipeline.js";
|
||||
|
||||
const settings = { reportMode: "draft-review" as const, githubTrackingDefaultRepo: "Runfusion/Fusion", githubAuthMode: "token", githubAuthToken: "test" };
|
||||
const settings = { reportRoadmapDedupeEnabled: false, reportMode: "draft-review" as const, githubTrackingDefaultRepo: "Runfusion/Fusion", githubAuthMode: "token", githubAuthToken: "test" };
|
||||
|
||||
function deps(overrides: Partial<ReportPipelineDeps> = {}): ReportPipelineDeps {
|
||||
return {
|
||||
@@ -45,35 +45,58 @@ describe("report pipeline", () => {
|
||||
expect(result.kind).toBe("draft-ready");
|
||||
});
|
||||
|
||||
it.each(["bug", "feedback", "idea", "help"] as const)("short-circuits %s reports that strongly match the roadmap without egress", async (actionType) => {
|
||||
const roadmapSource = vi.fn().mockResolvedValue([{ featureId: "RF-1", title: "Dashboard rendering controls", body: "Add dashboard rendering controls" }]);
|
||||
const context = deps({ projectSettings: { ...settings, reportMode: "auto-file", reportRoadmapDedup: true }, roadmapSource });
|
||||
const result = await runReportPipeline({ actionType, userPrompt: "Add dashboard rendering controls" }, context);
|
||||
expect(result).toMatchObject({ kind: "roadmap-match", roadmap: { featureId: "RF-1", title: "Dashboard rendering controls", description: "Add dashboard rendering controls" } });
|
||||
expect(roadmapSource).toHaveBeenCalledOnce();
|
||||
expect(context.client!.createIssue).not.toHaveBeenCalled();
|
||||
expect(context.client!.createDiscussion).not.toHaveBeenCalled();
|
||||
expect(context.client!.commentOnIssue).not.toHaveBeenCalled();
|
||||
expect(context.client!.commentOnDiscussion).not.toHaveBeenCalled();
|
||||
it.each(["bug", "feedback", "idea", "help"] as const)("deduplicates %s reports against an open labeled roadmap issue in auto-file mode", async (actionType) => {
|
||||
const client = {
|
||||
createIssue: vi.fn(), createDiscussion: vi.fn(), addIssueReaction: vi.fn(), commentOnIssue: vi.fn().mockResolvedValue({ url: "roadmap-comment" }),
|
||||
searchIssues: vi.fn().mockImplementation((_owner, _repo, query) => Promise.resolve(query.includes("label:roadmap") ? [{ number: 30, title: "dashboard rendering controls", body: "dashboard rendering controls", html_url: "roadmap-url", state: "open" }] : [])),
|
||||
searchDiscussions: vi.fn().mockResolvedValue([]),
|
||||
};
|
||||
const result = await runReportPipeline({ actionType, userPrompt: "Add dashboard rendering controls" }, deps({ projectSettings: { ...settings, reportMode: "auto-file", reportRoadmapDedupeEnabled: true }, client }));
|
||||
expect(result).toMatchObject({ kind: "endorsed", issueNumber: 30 });
|
||||
expect(client.addIssueReaction).toHaveBeenCalledWith("Runfusion", "Fusion", 30, "+1");
|
||||
expect(client.createIssue).not.toHaveBeenCalled();
|
||||
expect(client.createDiscussion).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not read the roadmap source when its opt-in setting is off", async () => {
|
||||
const roadmapSource = vi.fn().mockResolvedValue([{ featureId: "RF-1", title: "Dashboard rendering controls", body: "Add dashboard rendering controls" }]);
|
||||
const context = deps({ projectSettings: { ...settings, reportMode: "auto-file", reportRoadmapDedup: false }, roadmapSource });
|
||||
const result = await runReportPipeline({ actionType: "idea", userPrompt: "Add dashboard rendering controls" }, context);
|
||||
expect(result.kind).toBe("filed");
|
||||
expect(roadmapSource).not.toHaveBeenCalled();
|
||||
expect(context.client!.createIssue).toHaveBeenCalledOnce();
|
||||
it.each(["bug", "feedback"] as const)("returns an endorseable roadmap duplicate for %s reports in draft-review mode", async (actionType) => {
|
||||
const client = {
|
||||
createIssue: vi.fn(), addIssueReaction: vi.fn(), commentOnIssue: vi.fn(), searchDiscussions: vi.fn().mockResolvedValue([]),
|
||||
searchIssues: vi.fn().mockImplementation((_owner, _repo, query) => Promise.resolve(query.includes("label:roadmap") ? [{ number: 30, title: "dashboard rendering controls", body: "dashboard rendering controls", html_url: "roadmap-url", state: "open" }] : [])),
|
||||
};
|
||||
const result = await runReportPipeline({ actionType, userPrompt: "Add dashboard rendering controls" }, deps({ projectSettings: { ...settings, reportRoadmapDedupeEnabled: true }, client }));
|
||||
expect(result).toMatchObject({ kind: "duplicate-found", issue: { number: 30, roadmap: true } });
|
||||
expect(client.addIssueReaction).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls through to existing GitHub routing when roadmap candidates do not match", async () => {
|
||||
const context = deps({
|
||||
projectSettings: { ...settings, reportMode: "auto-file", reportRoadmapDedup: true },
|
||||
roadmapSource: vi.fn().mockResolvedValue([{ featureId: "RF-1", title: "Unrelated access controls", body: "Manage permissions" }]),
|
||||
});
|
||||
const result = await runReportPipeline({ actionType: "idea", userPrompt: "Add dashboard rendering controls" }, context);
|
||||
it("ignores closed or disabled roadmap items and falls through to filing", async () => {
|
||||
const client = { createIssue: vi.fn().mockResolvedValue({ htmlUrl: "filed" }), addIssueReaction: vi.fn(), commentOnIssue: vi.fn(), searchIssues: vi.fn().mockResolvedValue([{ number: 30, title: "dashboard rendering controls", body: "dashboard rendering controls", html_url: "roadmap-url", state: "closed" }]) };
|
||||
const result = await runReportPipeline({ actionType: "idea", userPrompt: "Add dashboard rendering controls" }, deps({ projectSettings: { ...settings, reportMode: "auto-file", reportRoadmapDedupeEnabled: false }, client }));
|
||||
expect(result.kind).toBe("filed");
|
||||
expect(context.client!.createIssue).toHaveBeenCalledOnce();
|
||||
expect(client.addIssueReaction).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("prefers a roadmap issue over a matching ordinary destination issue", async () => {
|
||||
const client = { createIssue: vi.fn(), addIssueReaction: vi.fn(), commentOnIssue: vi.fn().mockResolvedValue({ url: "roadmap-comment" }), searchIssues: vi.fn().mockImplementation((_owner, _repo, query) => Promise.resolve([{ number: query.includes("label:roadmap") ? 30 : 9, title: "dashboard rendering controls", body: "dashboard rendering controls", html_url: "url", state: "open" }])) };
|
||||
const result = await runReportPipeline({ actionType: "idea", userPrompt: "Add dashboard rendering controls" }, deps({ projectSettings: { ...settings, reportMode: "auto-file", reportRoadmapDedupeEnabled: true }, client }));
|
||||
expect(result).toMatchObject({ kind: "endorsed", issueNumber: 30 });
|
||||
});
|
||||
|
||||
it("re-verifies a roadmap endorsement and preserves session idempotency", async () => {
|
||||
const client = { createIssue: vi.fn(), addIssueReaction: vi.fn(), commentOnIssue: vi.fn().mockResolvedValue({ url: "roadmap-comment" }), searchIssues: vi.fn().mockImplementation((_owner, _repo, query) => Promise.resolve(query.includes("label:roadmap") ? [{ number: 30, title: "dashboard rendering controls", body: "dashboard rendering controls", html_url: "url", state: "open" }] : [])) };
|
||||
const report = { userPrompt: "Add dashboard rendering controls at /Users/alice/private-project", summary: "Add dashboard rendering controls", body: "secret path /Users/alice/private-project", context: {}, sessionToken: "roadmap-session" };
|
||||
const context = deps({ projectSettings: { ...settings, reportMode: "auto-file", reportRoadmapDedupeEnabled: true }, client });
|
||||
await runReportPipeline({ actionType: "idea", userPrompt: report.userPrompt }, context, { file: true, endorseRoadmapIssueNumber: 30, report });
|
||||
await runReportPipeline({ actionType: "idea", userPrompt: report.userPrompt }, context, { file: true, endorseRoadmapIssueNumber: 30, report });
|
||||
expect(client.addIssueReaction).toHaveBeenCalledOnce();
|
||||
expect(client.commentOnIssue).toHaveBeenCalledOnce();
|
||||
expect(String(client.commentOnIssue.mock.calls[0][3])).not.toContain("private-project");
|
||||
});
|
||||
|
||||
it("resolves roadmap settings project then global then defaults", async () => {
|
||||
const { resolveRoadmapDedupe } = await import("../report-pipeline.js");
|
||||
expect(resolveRoadmapDedupe({ projectSettings: { ...settings, reportRoadmapDedupeEnabled: false }, globalSettings: { reportRoadmapDedupeEnabled: true } }).enabled).toBe(false);
|
||||
expect(resolveRoadmapDedupe({ projectSettings: settings, globalSettings: { reportRoadmapDedupeEnabled: false, reportRoadmapLabel: "planned", reportRoadmapRepo: "other/tracker" } })).toMatchObject({ enabled: false, label: "planned", repo: { owner: "other", repo: "tracker" } });
|
||||
expect(resolveRoadmapDedupe({ projectSettings: settings }).label).toBe("roadmap");
|
||||
});
|
||||
|
||||
it("files in auto-file mode", async () => {
|
||||
|
||||
@@ -40,23 +40,18 @@ async function invoke(handler: (req: { body?: unknown }, res: { json: (body: unk
|
||||
|
||||
describe("report routes", () => {
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
it("passes an opted-in project roadmap source through both report routes", async () => {
|
||||
it("passes roadmap settings and a roadmap endorsement target to the pipeline", async () => {
|
||||
vi.mocked(queryKnowledgePagesAsync).mockResolvedValue([]);
|
||||
vi.mocked(runReportPipeline).mockResolvedValue({ kind: "roadmap-match" } as never);
|
||||
const handlers = setup({ reportMode: "auto-file", reportRoadmapDedup: true });
|
||||
await invoke(handlers.get("/report/draft")!, { actionType: "idea", userPrompt: "Dashboard report controls" });
|
||||
await invoke(handlers.get("/report/file")!, { actionType: "idea", report: { userPrompt: "Dashboard report controls", context: {} } });
|
||||
for (const [, deps] of vi.mocked(runReportPipeline).mock.calls) expect(deps.roadmapSource).toEqual(expect.any(Function));
|
||||
vi.mocked(runReportPipeline).mockResolvedValue({ kind: "duplicate-found" } as never);
|
||||
const handlers = setup({ reportMode: "auto-file", reportRoadmapDedupeEnabled: true, reportRoadmapLabel: "roadmap" });
|
||||
await invoke(handlers.get("/report/file")!, { actionType: "idea", endorseRoadmapIssueNumber: 30, report: { userPrompt: "Dashboard report controls", body: "/Users/alice/private-project", context: {} } });
|
||||
const [input, deps, options] = vi.mocked(runReportPipeline).mock.calls.at(-1)!;
|
||||
expect(deps.projectSettings).toMatchObject({ reportRoadmapDedupeEnabled: true, reportRoadmapLabel: "roadmap" });
|
||||
expect(options).toMatchObject({ endorseRoadmapIssueNumber: 30 });
|
||||
expect((options!.report as { body: string }).body).not.toContain("private-project");
|
||||
expect(input.actionType).toBe("idea");
|
||||
});
|
||||
|
||||
it("does not create a roadmap source while the setting is off", async () => {
|
||||
vi.mocked(runReportPipeline).mockResolvedValue({ kind: "draft-ready" } as never);
|
||||
const handlers = setup();
|
||||
await invoke(handlers.get("/report/draft")!, { actionType: "idea", userPrompt: "Dashboard report controls" });
|
||||
expect(vi.mocked(runReportPipeline).mock.calls.at(-1)?.[1].roadmapSource).toBeUndefined();
|
||||
});
|
||||
|
||||
|
||||
const PNG_SCREENSHOT = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9WlYk8sAAAAASUVORK5CYII=";
|
||||
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import type { GlobalSettings, ProjectSettings, ReportActionType, ReportMode } from "@fusion/core";
|
||||
import { resolveTaskGithubTracking } from "@fusion/core";
|
||||
import { parseRepoSlug, resolveTaskGithubTracking } from "@fusion/core";
|
||||
import { GitHubClient } from "./github.js";
|
||||
import { resolveGithubTrackingAuth } from "./github-auth.js";
|
||||
import { buildIssueSearchQueries, DEDUP_MATCH_THRESHOLD, scoreCandidateIssue } from "./github-tracking-dedup.js";
|
||||
import { scrubReportPayload, type ReportScrubContext } from "./report-scrub.js";
|
||||
import type { RoadmapDedupSource } from "./report-roadmap-source.js";
|
||||
|
||||
export type { ReportActionType, ReportMode };
|
||||
|
||||
@@ -36,20 +35,18 @@ export interface StructuredReport {
|
||||
|
||||
export type ReportResult =
|
||||
| { kind: "draft-ready"; report: StructuredReport; mode: ReportMode }
|
||||
| { kind: "duplicate-found"; report: StructuredReport; mode: ReportMode; issue: { number: number; url: string; title: string; discussionId?: string } }
|
||||
| { kind: "roadmap-match"; report: StructuredReport; mode: ReportMode; roadmap: { featureId: string; title: string; description: string } }
|
||||
| { kind: "duplicate-found"; report: StructuredReport; mode: ReportMode; issue: { number: number; url: string; title: string; discussionId?: string; roadmap?: true } }
|
||||
| { kind: "filed"; url: string; report: StructuredReport; screenshotNotAttached?: boolean }
|
||||
| { kind: "endorsed"; url: string; issueNumber: number; report: StructuredReport; screenshotNotAttached?: boolean }
|
||||
|
||||
| { kind: "unavailable"; reason: string; message: string };
|
||||
|
||||
export interface ReportPipelineDeps {
|
||||
projectSettings: Pick<ProjectSettings, "reportMode" | "reportModeByAction" | "reportRoadmapDedup" | "githubTrackingDefaultRepo" | "githubAuthMode" | "githubAuthToken">;
|
||||
projectSettings: Pick<ProjectSettings, "reportMode" | "reportModeByAction" | "reportRoadmapDedupeEnabled" | "reportRoadmapLabel" | "reportRoadmapRepo" | "githubTrackingDefaultRepo" | "githubAuthMode" | "githubAuthToken">;
|
||||
globalSettings?: Partial<GlobalSettings>;
|
||||
client?: Pick<GitHubClient, "createIssue" | "searchIssues" | "commentOnIssue" | "addIssueReaction"> & Partial<Pick<GitHubClient, "searchDiscussions" | "createDiscussion" | "commentOnDiscussion" | "addDiscussionReaction" | "uploadReportImage" | "deleteReportImage">>;
|
||||
scrubContext?: ReportScrubContext;
|
||||
gatherContext?: (input: ReportInput) => Promise<Record<string, unknown>>;
|
||||
roadmapSource?: RoadmapDedupSource;
|
||||
}
|
||||
|
||||
const MAX_PROMPT_LENGTH = 4_000;
|
||||
@@ -145,14 +142,39 @@ async function findDuplicate(client: NonNullable<ReportPipelineDeps["client"]>,
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function findRoadmapMatch(roadmapSource: RoadmapDedupSource, report: StructuredReport) {
|
||||
|
||||
export interface ResolvedRoadmapDedupe {
|
||||
enabled: boolean;
|
||||
label: string;
|
||||
repo: { owner: string; repo: string } | null;
|
||||
}
|
||||
|
||||
export function resolveRoadmapDedupe(deps: Pick<ReportPipelineDeps, "projectSettings" | "globalSettings">): ResolvedRoadmapDedupe {
|
||||
const project = deps.projectSettings;
|
||||
const global = deps.globalSettings;
|
||||
const enabled = project.reportRoadmapDedupeEnabled ?? global?.reportRoadmapDedupeEnabled ?? true;
|
||||
const label = (project.reportRoadmapLabel ?? global?.reportRoadmapLabel ?? "roadmap").trim();
|
||||
const trackingRepo = resolveRepo(deps as ReportPipelineDeps);
|
||||
const repo = parseRepoSlug(project.reportRoadmapRepo ?? global?.reportRoadmapRepo) ?? trackingRepo;
|
||||
return { enabled: enabled && Boolean(label), label, repo };
|
||||
}
|
||||
|
||||
async function findRoadmapDuplicate(client: NonNullable<ReportPipelineDeps["client"]>, roadmap: ResolvedRoadmapDedupe, report: StructuredReport) {
|
||||
if (!roadmap.enabled || !roadmap.repo) return undefined;
|
||||
const keywords = reportKeywords(report);
|
||||
const candidates = await roadmapSource(keywords);
|
||||
const match = candidates
|
||||
.map((candidate) => ({ candidate, score: scoreCandidateIssue(candidate, [], keywords).score }))
|
||||
.filter(({ score }) => score >= DEDUP_MATCH_THRESHOLD)
|
||||
.sort((left, right) => right.score - left.score)[0];
|
||||
return match?.candidate;
|
||||
try {
|
||||
for (const query of buildIssueSearchQueries([], keywords)) {
|
||||
const candidates = await client.searchIssues(roadmap.repo.owner, roadmap.repo.repo, `label:${roadmap.label} ${query}`, { state: "open", limit: 20 });
|
||||
const match = candidates.filter((candidate) => candidate.state === "open")
|
||||
.map((candidate) => ({ candidate, score: scoreCandidateIssue(candidate, [], keywords).score }))
|
||||
.sort((left, right) => right.score - left.score)
|
||||
.find(({ score }) => score >= DEDUP_MATCH_THRESHOLD);
|
||||
if (match) return match.candidate;
|
||||
}
|
||||
} catch {
|
||||
// An optional public roadmap must never block the established destination dedupe path.
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function approvedReportImageUrl(candidate: string | undefined, owner: string, repo: string): string | undefined {
|
||||
@@ -272,7 +294,7 @@ function normalizeSubmittedReport(input: ReportInput, gathered: Record<string, u
|
||||
};
|
||||
}
|
||||
|
||||
export async function runReportPipeline(input: ReportInput, deps: ReportPipelineDeps, options: { file?: boolean; endorseIssueNumber?: number; endorseDiscussionId?: string; report?: StructuredReport } = {}): Promise<ReportResult> {
|
||||
export async function runReportPipeline(input: ReportInput, deps: ReportPipelineDeps, options: { file?: boolean; endorseIssueNumber?: number; endorseDiscussionId?: string; endorseRoadmapIssueNumber?: number; report?: StructuredReport } = {}): Promise<ReportResult> {
|
||||
const gathered = await deps.gatherContext?.(input) ?? { taskId: input.contextRefs?.taskId, agentId: input.contextRefs?.agentId };
|
||||
const normalized = normalizeSubmittedReport(input, gathered, options.report);
|
||||
// FNXC:ReportPipeline 2026-07-18-14:30: Screenshot data is validated by the
|
||||
@@ -282,25 +304,18 @@ export async function runReportPipeline(input: ReportInput, deps: ReportPipeline
|
||||
let report: StructuredReport = { ...scrubReportPayload(textualReport, deps.scrubContext), ...(input.screenshot ? { screenshot: input.screenshot } : {}) };
|
||||
|
||||
const mode = resolveReportMode(input.actionType, deps.projectSettings);
|
||||
/*
|
||||
FNXC:ReportPipeline 2026-07-18-12:30:
|
||||
An opted-in roadmap hit takes deterministic precedence over GitHub matching
|
||||
and filing. It returns local roadmap context only, so no issue, discussion,
|
||||
comment, reaction, or other external egress can occur in either report mode.
|
||||
*/
|
||||
if (deps.projectSettings.reportRoadmapDedup && deps.roadmapSource) {
|
||||
const roadmap = await findRoadmapMatch(deps.roadmapSource, report);
|
||||
if (roadmap) return {
|
||||
kind: "roadmap-match",
|
||||
report,
|
||||
mode,
|
||||
roadmap: { featureId: roadmap.featureId, title: roadmap.title, description: roadmap.body ?? "" },
|
||||
};
|
||||
}
|
||||
const clientResult = createClient(deps);
|
||||
if (clientResult.unavailable) return clientResult.unavailable;
|
||||
const repo = resolveRepo(deps);
|
||||
if (!repo || !clientResult.client) return { kind: "unavailable", reason: "repo_missing", message: "Configure a GitHub tracking repository before filing reports." };
|
||||
const roadmap = resolveRoadmapDedupe(deps);
|
||||
/*
|
||||
FNXC:ReportPipeline 2026-07-18-20:15:
|
||||
FR-30 public-roadmap issues are an additive, OPEN-only dedupe source. A roadmap
|
||||
hit deterministically wins over destination matches, and endorsement reuses the
|
||||
issue +1/scrub path; unavailable roadmap search falls through without egress.
|
||||
*/
|
||||
const roadmapDuplicate = await findRoadmapDuplicate(clientResult.client, roadmap, report);
|
||||
const shouldAttachScreenshot = Boolean(input.screenshot) && (options.file || mode === "auto-file");
|
||||
const destination = destinationFor(input.actionType);
|
||||
// FNXC:ReportPipeline 2026-07-18-19:30: Validate and publish the scrubbed
|
||||
@@ -315,6 +330,13 @@ export async function runReportPipeline(input: ReportInput, deps: ReportPipeline
|
||||
shouldAttachScreenshot ? input.screenshot : undefined, clientResult.client!, repo.owner, repo.repo,
|
||||
async (url) => { await clientResult.client!.commentOnDiscussion!(discussionId, `## Screenshot\n`); },
|
||||
);
|
||||
if (options.endorseRoadmapIssueNumber) {
|
||||
if (!roadmapDuplicate || roadmapDuplicate.number !== options.endorseRoadmapIssueNumber || !roadmap.repo) {
|
||||
return { kind: "unavailable", reason: "duplicate_not_verified", message: "The selected roadmap item is no longer an open matching report. Please prepare the report again." };
|
||||
}
|
||||
const endorsed = await endorseDuplicate({ owner: roadmap.repo.owner, repo: roadmap.repo.repo, issueNumber: roadmapDuplicate.number, report, client: clientResult.client, scrubContext: deps.scrubContext });
|
||||
return endorsed;
|
||||
}
|
||||
if (options.endorseDiscussionId) {
|
||||
if (!clientResult.client.commentOnDiscussion || !clientResult.client.addDiscussionReaction || destination !== "discussion" || duplicate?.discussionId !== options.endorseDiscussionId) {
|
||||
return { kind: "unavailable", reason: "duplicate_not_verified", message: "The selected discussion is no longer an open matching report. Please prepare the report again." };
|
||||
@@ -331,6 +353,10 @@ export async function runReportPipeline(input: ReportInput, deps: ReportPipeline
|
||||
const attachment = await attachToIssue(duplicate.number);
|
||||
return { ...endorsed, report: appendReviewedScreenshot(endorsed.report, attachment.screenshotUrl), ...(attachment.screenshotNotAttached ? { screenshotNotAttached: true } : {}) };
|
||||
}
|
||||
if (roadmapDuplicate) {
|
||||
if (mode === "auto-file") return endorseDuplicate({ owner: roadmap.repo!.owner, repo: roadmap.repo!.repo, issueNumber: roadmapDuplicate.number, report, client: clientResult.client, scrubContext: deps.scrubContext });
|
||||
return { kind: "duplicate-found", report, mode, issue: { number: roadmapDuplicate.number, url: roadmapDuplicate.html_url, title: roadmapDuplicate.title, roadmap: true } };
|
||||
}
|
||||
if (duplicate) {
|
||||
if (mode === "auto-file") {
|
||||
if (destination === "discussion") {
|
||||
|
||||
@@ -2,7 +2,6 @@ import { ApiError } from "../api-error.js";
|
||||
import { queryKnowledgePagesAsync } from "../knowledge-index.js";
|
||||
import { requireAsyncLayer } from "../require-async-layer.js";
|
||||
import { runReportPipeline, type ReportInput, type StructuredReport } from "../report-pipeline.js";
|
||||
import { createRoadmapDedupSourceForTaskStore } from "../report-roadmap-source.js";
|
||||
import { scrubReportPayload } from "../report-scrub.js";
|
||||
import { selfCheckHelp } from "../report-help-selfcheck.js";
|
||||
import type { ApiRouteRegistrar } from "./types.js";
|
||||
@@ -103,7 +102,6 @@ export const registerReportRoutes: ApiRouteRegistrar = ({ router, getScopedStore
|
||||
globalSettings: scopes.global,
|
||||
scrubContext: { rootDir: store.getRootDir(), projectName: store.getRootDir().split(/[\\/]/).pop() },
|
||||
gatherContext: (reportInput) => gatherReportContext(store, reportInput, scopes.project as Record<string, unknown>),
|
||||
roadmapSource: scopes.project.reportRoadmapDedup ? createRoadmapDedupSourceForTaskStore(store) : undefined,
|
||||
});
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
@@ -137,6 +135,10 @@ export const registerReportRoutes: ApiRouteRegistrar = ({ router, getScopedStore
|
||||
const validatedInput = input;
|
||||
const endorseIssueNumber = typeof raw.endorseIssueNumber === "number" ? raw.endorseIssueNumber : undefined;
|
||||
const endorseDiscussionId = typeof raw.endorseDiscussionId === "string" ? raw.endorseDiscussionId : undefined;
|
||||
// FNXC:ReportPipeline 2026-07-18-20:30: A browser-supplied public-roadmap
|
||||
// issue number is never authority to comment. The pipeline re-searches the
|
||||
// OPEN label-qualified item and re-scrubs this editable payload before egress.
|
||||
const endorseRoadmapIssueNumber = typeof raw.endorseRoadmapIssueNumber === "number" ? raw.endorseRoadmapIssueNumber : undefined;
|
||||
const help = await selfCheckHelpBeforePipeline(store, validatedInput);
|
||||
if (help?.answered) {
|
||||
res.json({ kind: "help", answer: help.answer });
|
||||
@@ -147,8 +149,7 @@ export const registerReportRoutes: ApiRouteRegistrar = ({ router, getScopedStore
|
||||
globalSettings: scopes.global,
|
||||
scrubContext: { rootDir: store.getRootDir(), projectName: store.getRootDir().split(/[\\/]/).pop() },
|
||||
gatherContext: (reportInput) => gatherReportContext(store, reportInput, scopes.project as Record<string, unknown>),
|
||||
roadmapSource: scopes.project.reportRoadmapDedup ? createRoadmapDedupSourceForTaskStore(store) : undefined,
|
||||
}, { file: true, endorseIssueNumber, endorseDiscussionId, report: untrusted });
|
||||
}, { file: true, endorseIssueNumber, endorseDiscussionId, endorseRoadmapIssueNumber, report: untrusted });
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError) throw error;
|
||||
|
||||
@@ -5959,8 +5959,12 @@
|
||||
"aiUndoTaskWorkflowHelp": "Workflow assigned to AI-undo (revert) tasks, which reverse already-shipped code and warrant stricter review. Choose \"Inherit project default workflow\" to leave them on the project default. Default: review-heavy.",
|
||||
"reportModeHelp": "How in-app bug/feedback/idea/help reports are filed. Default: draft-review (operator reviews a draft before filing).",
|
||||
"reportModeByActionHelp": "Optional per-action override of the project report mode for bug, feedback, idea, or help. No default — unset actions inherit reportMode.",
|
||||
"reportRoadmapDedup": "Check roadmap before filing reports",
|
||||
"reportRoadmapDedupHelp": "When enabled, matching roadmap features are shown inline instead of filing another GitHub Issue or Discussion. Default: off."
|
||||
"reportRoadmapRepo": "Public roadmap repository (optional)",
|
||||
"reportRoadmapRepoHelp": "GitHub owner/repository containing public roadmap issues. When unset, uses the tracking repository. No default — unset.",
|
||||
"reportRoadmapDedupeEnabled": "Deduplicate reports against public roadmap",
|
||||
"reportRoadmapDedupeEnabledHelp": "Match open, labeled roadmap issues before filing. Default: on.",
|
||||
"reportRoadmapLabel": "Public roadmap label",
|
||||
"reportRoadmapLabelHelp": "Open GitHub Issues with this label are considered roadmap items. Default: roadmap."
|
||||
},
|
||||
"globalGeneral": {
|
||||
"andShowsUpdateNoticesInTheCLIAnd": " and shows update notices in the CLI and dashboard. Cadence is governed by the frequency below. Default: enabled. ",
|
||||
@@ -5975,6 +5979,12 @@
|
||||
"frequency": "Frequency",
|
||||
"general": "General",
|
||||
"globalDefaultTrackingRepo": "Global default tracking repo",
|
||||
"reportRoadmapDedupeEnabled": "Global public-roadmap deduplication",
|
||||
"reportRoadmapDedupeEnabledHelp": "Fallback for projects that do not set a public-roadmap deduplication preference. No default — unset (projects default to enabled).",
|
||||
"reportRoadmapLabel": "Global public roadmap label",
|
||||
"reportRoadmapLabelHelp": "Fallback label for open public-roadmap issues when a project does not set one. No default — unset (projects default to roadmap).",
|
||||
"reportRoadmapRepo": "Global public roadmap repository (optional)",
|
||||
"reportRoadmapRepoHelp": "Fallback GitHub owner/repository for public-roadmap issues. When unset, uses the tracking repository. No default — unset.",
|
||||
"leaveBothThinkingTogglesOffToKeepThe": " Leave both thinking toggles off to keep the original default behavior. This only controls persisted ",
|
||||
"manualOnlyNeverAutoCheck": "Manual only — never auto-check",
|
||||
"onStartupOncePerServerLaunch": "On startup — once per server launch",
|
||||
@@ -9142,6 +9152,9 @@
|
||||
"roadmapMatch": {
|
||||
"title": "Already on the roadmap",
|
||||
"message": "This report matches a feature that is already planned."
|
||||
},
|
||||
"roadmapDuplicate": {
|
||||
"title": "Already on the roadmap — add your data point?"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5946,10 +5946,14 @@
|
||||
"ephemeralAgentTaskCreationPolicyDeny": "Denegar",
|
||||
"reportModeHelp": "Cómo se archivan los informes in-app (error, feedback, idea, ayuda). Predeterminado: draft-review (el operador revisa un borrador antes de archivar).",
|
||||
"reportModeByActionHelp": "Anulación opcional por acción del modo de informe del proyecto (error, feedback, idea o ayuda). Sin valor predeterminado: las acciones no definidas heredan reportMode.",
|
||||
"reportRoadmapRepo": "Public roadmap repository (optional)",
|
||||
"reportRoadmapRepoHelp": "GitHub owner/repository containing public roadmap issues. When unset, uses the tracking repository. No default — unset.",
|
||||
"reviewArtifacts": "Artefactos de revisión",
|
||||
"reviewArtifactsHint": "Controla si las tareas futuras elegibles generan entregables de revisión. Los informes de usuario y la captura de actividad se mantienen independientes. Predeterminado: desactivado.",
|
||||
"reportRoadmapDedup": "Comprobar la hoja de ruta antes de enviar informes",
|
||||
"reportRoadmapDedupHelp": "Cuando está activado, las funciones coincidentes se muestran en línea en lugar de crear otro problema o discusión de GitHub. Predeterminado: desactivado."
|
||||
"reportRoadmapDedupeEnabled": "Deduplicate reports against public roadmap",
|
||||
"reportRoadmapDedupeEnabledHelp": "Match open, labeled roadmap issues before filing. Default: on.",
|
||||
"reportRoadmapLabel": "Public roadmap label",
|
||||
"reportRoadmapLabelHelp": "Open GitHub Issues with this label are considered roadmap items. Default: roadmap."
|
||||
},
|
||||
"globalGeneral": {
|
||||
"andShowsUpdateNoticesInTheCLIAnd": "",
|
||||
@@ -5964,6 +5968,12 @@
|
||||
"frequency": "",
|
||||
"general": "",
|
||||
"globalDefaultTrackingRepo": "",
|
||||
"reportRoadmapDedupeEnabled": "Global public-roadmap deduplication",
|
||||
"reportRoadmapDedupeEnabledHelp": "Fallback for projects that do not set a public-roadmap deduplication preference. No default — unset (projects default to enabled).",
|
||||
"reportRoadmapLabel": "Global public roadmap label",
|
||||
"reportRoadmapLabelHelp": "Fallback label for open public-roadmap issues when a project does not set one. No default — unset (projects default to roadmap).",
|
||||
"reportRoadmapRepo": "Global public roadmap repository (optional)",
|
||||
"reportRoadmapRepoHelp": "Fallback GitHub owner/repository for public-roadmap issues. When unset, uses the tracking repository. No default — unset.",
|
||||
"leaveBothThinkingTogglesOffToKeepThe": "",
|
||||
"manualOnlyNeverAutoCheck": "",
|
||||
"onStartupOncePerServerLaunch": "",
|
||||
@@ -9141,6 +9151,9 @@
|
||||
"roadmapMatch": {
|
||||
"title": "Ya está en la hoja de ruta",
|
||||
"message": "Este informe coincide con una función que ya está planificada."
|
||||
},
|
||||
"roadmapDuplicate": {
|
||||
"title": "Already on the roadmap — add your data point?"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5946,10 +5946,14 @@
|
||||
"ephemeralAgentTaskCreationPolicyDeny": "Refuser",
|
||||
"reportModeHelp": "Comment les rapports in-app (bug, feedback, idée, aide) sont déposés. Par défaut : draft-review (l'opérateur revoit un brouillon avant dépôt).",
|
||||
"reportModeByActionHelp": "Surcharge optionnelle par action du mode de rapport du projet (bug, feedback, idée ou aide). Aucune valeur par défaut — les actions non définies héritent de reportMode.",
|
||||
"reportRoadmapRepo": "Public roadmap repository (optional)",
|
||||
"reportRoadmapRepoHelp": "GitHub owner/repository containing public roadmap issues. When unset, uses the tracking repository. No default — unset.",
|
||||
"reviewArtifacts": "Artefacts de revue",
|
||||
"reviewArtifactsHint": "Contrôle si les futures tâches éligibles génèrent des livrables de revue. Les rapports utilisateur et la capture d'activité restent séparés. Par défaut : désactivé.",
|
||||
"reportRoadmapDedup": "Vérifier la feuille de route avant d’envoyer les rapports",
|
||||
"reportRoadmapDedupHelp": "Lorsqu’elle est activée, les fonctionnalités correspondantes sont affichées en ligne au lieu de créer un autre problème ou une autre discussion GitHub. Par défaut : désactivé."
|
||||
"reportRoadmapDedupeEnabled": "Deduplicate reports against public roadmap",
|
||||
"reportRoadmapDedupeEnabledHelp": "Match open, labeled roadmap issues before filing. Default: on.",
|
||||
"reportRoadmapLabel": "Public roadmap label",
|
||||
"reportRoadmapLabelHelp": "Open GitHub Issues with this label are considered roadmap items. Default: roadmap."
|
||||
},
|
||||
"globalGeneral": {
|
||||
"andShowsUpdateNoticesInTheCLIAnd": "",
|
||||
@@ -5964,6 +5968,12 @@
|
||||
"frequency": "",
|
||||
"general": "",
|
||||
"globalDefaultTrackingRepo": "",
|
||||
"reportRoadmapDedupeEnabled": "Global public-roadmap deduplication",
|
||||
"reportRoadmapDedupeEnabledHelp": "Fallback for projects that do not set a public-roadmap deduplication preference. No default — unset (projects default to enabled).",
|
||||
"reportRoadmapLabel": "Global public roadmap label",
|
||||
"reportRoadmapLabelHelp": "Fallback label for open public-roadmap issues when a project does not set one. No default — unset (projects default to roadmap).",
|
||||
"reportRoadmapRepo": "Global public roadmap repository (optional)",
|
||||
"reportRoadmapRepoHelp": "Fallback GitHub owner/repository for public-roadmap issues. When unset, uses the tracking repository. No default — unset.",
|
||||
"leaveBothThinkingTogglesOffToKeepThe": "",
|
||||
"manualOnlyNeverAutoCheck": "",
|
||||
"onStartupOncePerServerLaunch": "",
|
||||
@@ -9141,6 +9151,9 @@
|
||||
"roadmapMatch": {
|
||||
"title": "Déjà sur la feuille de route",
|
||||
"message": "Ce signalement correspond à une fonctionnalité déjà planifiée."
|
||||
},
|
||||
"roadmapDuplicate": {
|
||||
"title": "Already on the roadmap — add your data point?"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5946,10 +5946,14 @@
|
||||
"ephemeralAgentTaskCreationPolicyDeny": "거부",
|
||||
"reportModeHelp": "인앱 버그/피드백/아이디어/도움말 리포트 제출 방식. 기본값: draft-review(제출 전 운영자가 초안 검토).",
|
||||
"reportModeByActionHelp": "버그·피드백·아이디어·도움말 등 작업별 프로젝트 리포트 모드 선택 재정의. 기본값 없음 — 미설정 작업은 reportMode를 상속합니다.",
|
||||
"reportRoadmapRepo": "Public roadmap repository (optional)",
|
||||
"reportRoadmapRepoHelp": "GitHub owner/repository containing public roadmap issues. When unset, uses the tracking repository. No default — unset.",
|
||||
"reviewArtifacts": "리뷰 산출물",
|
||||
"reviewArtifactsHint": "적격한 향후 작업이 리뷰 산출물을 생성할지 제어합니다. 사용자 보고와 활동 캡처는 별도로 유지됩니다. 기본값: 꺼짐.",
|
||||
"reportRoadmapDedup": "보고서를 제출하기 전에 로드맵 확인",
|
||||
"reportRoadmapDedupHelp": "활성화하면 일치하는 로드맵 기능이 다른 GitHub 이슈나 토론을 만들지 않고 인라인으로 표시됩니다. 기본값: 꺼짐."
|
||||
"reportRoadmapDedupeEnabled": "Deduplicate reports against public roadmap",
|
||||
"reportRoadmapDedupeEnabledHelp": "Match open, labeled roadmap issues before filing. Default: on.",
|
||||
"reportRoadmapLabel": "Public roadmap label",
|
||||
"reportRoadmapLabelHelp": "Open GitHub Issues with this label are considered roadmap items. Default: roadmap."
|
||||
},
|
||||
"globalGeneral": {
|
||||
"andShowsUpdateNoticesInTheCLIAnd": "",
|
||||
@@ -5964,6 +5968,12 @@
|
||||
"frequency": "",
|
||||
"general": "",
|
||||
"globalDefaultTrackingRepo": "",
|
||||
"reportRoadmapDedupeEnabled": "Global public-roadmap deduplication",
|
||||
"reportRoadmapDedupeEnabledHelp": "Fallback for projects that do not set a public-roadmap deduplication preference. No default — unset (projects default to enabled).",
|
||||
"reportRoadmapLabel": "Global public roadmap label",
|
||||
"reportRoadmapLabelHelp": "Fallback label for open public-roadmap issues when a project does not set one. No default — unset (projects default to roadmap).",
|
||||
"reportRoadmapRepo": "Global public roadmap repository (optional)",
|
||||
"reportRoadmapRepoHelp": "Fallback GitHub owner/repository for public-roadmap issues. When unset, uses the tracking repository. No default — unset.",
|
||||
"leaveBothThinkingTogglesOffToKeepThe": "",
|
||||
"manualOnlyNeverAutoCheck": "",
|
||||
"onStartupOncePerServerLaunch": "",
|
||||
@@ -9141,6 +9151,9 @@
|
||||
"roadmapMatch": {
|
||||
"title": "이미 로드맵에 있습니다",
|
||||
"message": "이 보고서는 이미 계획된 기능과 일치합니다."
|
||||
},
|
||||
"roadmapDuplicate": {
|
||||
"title": "Already on the roadmap — add your data point?"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5946,10 +5946,14 @@
|
||||
"ephemeralAgentTaskCreationPolicyDeny": "拒绝",
|
||||
"reportModeHelp": "应用内缺陷/反馈/想法/帮助报告的提交方式。默认:draft-review(提交前由操作者审阅草稿)。",
|
||||
"reportModeByActionHelp": "可选的按操作覆盖项目报告模式(缺陷、反馈、想法或帮助)。无默认值 — 未设置的操作继承 reportMode。",
|
||||
"reportRoadmapDedupeEnabled": "Deduplicate reports against public roadmap",
|
||||
"reportRoadmapDedupeEnabledHelp": "Match open, labeled roadmap issues before filing. Default: on.",
|
||||
"reportRoadmapLabel": "Public roadmap label",
|
||||
"reportRoadmapLabelHelp": "Open GitHub Issues with this label are considered roadmap items. Default: roadmap.",
|
||||
"reportRoadmapRepo": "Public roadmap repository (optional)",
|
||||
"reportRoadmapRepoHelp": "GitHub owner/repository containing public roadmap issues. When unset, uses the tracking repository. No default — unset.",
|
||||
"reviewArtifacts": "评审产物",
|
||||
"reviewArtifactsHint": "控制符合条件的未来任务是否生成评审交付物。面向用户的报告与活动捕获保持独立。默认:关闭。",
|
||||
"reportRoadmapDedup": "提交报告前检查路线图",
|
||||
"reportRoadmapDedupHelp": "启用后,匹配的路线图功能会以内联方式显示,而不会创建另一个 GitHub Issue 或讨论。默认:关闭。"
|
||||
"reviewArtifactsHint": "控制符合条件的未来任务是否生成评审交付物。面向用户的报告与活动捕获保持独立。默认:关闭。"
|
||||
},
|
||||
"globalGeneral": {
|
||||
"andShowsUpdateNoticesInTheCLIAnd": "",
|
||||
@@ -5964,6 +5968,12 @@
|
||||
"frequency": "",
|
||||
"general": "",
|
||||
"globalDefaultTrackingRepo": "",
|
||||
"reportRoadmapDedupeEnabled": "Global public-roadmap deduplication",
|
||||
"reportRoadmapDedupeEnabledHelp": "Fallback for projects that do not set a public-roadmap deduplication preference. No default — unset (projects default to enabled).",
|
||||
"reportRoadmapLabel": "Global public roadmap label",
|
||||
"reportRoadmapLabelHelp": "Fallback label for open public-roadmap issues when a project does not set one. No default — unset (projects default to roadmap).",
|
||||
"reportRoadmapRepo": "Global public roadmap repository (optional)",
|
||||
"reportRoadmapRepoHelp": "Fallback GitHub owner/repository for public-roadmap issues. When unset, uses the tracking repository. No default — unset.",
|
||||
"leaveBothThinkingTogglesOffToKeepThe": "",
|
||||
"manualOnlyNeverAutoCheck": "",
|
||||
"onStartupOncePerServerLaunch": "",
|
||||
|
||||
@@ -5946,10 +5946,14 @@
|
||||
"ephemeralAgentTaskCreationPolicyDeny": "拒絕",
|
||||
"reportModeHelp": "應用內缺陷/回饋/想法/說明報告的提交方式。預設:draft-review(提交前由操作者審閱草稿)。",
|
||||
"reportModeByActionHelp": "可選的依操作覆寫專案報告模式(缺陷、回饋、想法或說明)。無預設值 — 未設定的操作繼承 reportMode。",
|
||||
"reportRoadmapDedupeEnabled": "Deduplicate reports against public roadmap",
|
||||
"reportRoadmapDedupeEnabledHelp": "Match open, labeled roadmap issues before filing. Default: on.",
|
||||
"reportRoadmapLabel": "Public roadmap label",
|
||||
"reportRoadmapLabelHelp": "Open GitHub Issues with this label are considered roadmap items. Default: roadmap.",
|
||||
"reportRoadmapRepo": "Public roadmap repository (optional)",
|
||||
"reportRoadmapRepoHelp": "GitHub owner/repository containing public roadmap issues. When unset, uses the tracking repository. No default — unset.",
|
||||
"reviewArtifacts": "審查產物",
|
||||
"reviewArtifactsHint": "控制符合條件的未來任務是否產生審查交付物。面向使用者的報告與活動擷取保持獨立。預設:關閉。",
|
||||
"reportRoadmapDedup": "提交報告前檢查路線圖",
|
||||
"reportRoadmapDedupHelp": "啟用後,符合的路線圖功能會內嵌顯示,而不會建立另一個 GitHub Issue 或討論。預設:關閉。"
|
||||
"reviewArtifactsHint": "控制符合條件的未來任務是否產生審查交付物。面向使用者的報告與活動擷取保持獨立。預設:關閉。"
|
||||
},
|
||||
"globalGeneral": {
|
||||
"andShowsUpdateNoticesInTheCLIAnd": "",
|
||||
@@ -5964,6 +5968,12 @@
|
||||
"frequency": "",
|
||||
"general": "",
|
||||
"globalDefaultTrackingRepo": "",
|
||||
"reportRoadmapDedupeEnabled": "Global public-roadmap deduplication",
|
||||
"reportRoadmapDedupeEnabledHelp": "Fallback for projects that do not set a public-roadmap deduplication preference. No default — unset (projects default to enabled).",
|
||||
"reportRoadmapLabel": "Global public roadmap label",
|
||||
"reportRoadmapLabelHelp": "Fallback label for open public-roadmap issues when a project does not set one. No default — unset (projects default to roadmap).",
|
||||
"reportRoadmapRepo": "Global public roadmap repository (optional)",
|
||||
"reportRoadmapRepoHelp": "Fallback GitHub owner/repository for public-roadmap issues. When unset, uses the tracking repository. No default — unset.",
|
||||
"leaveBothThinkingTogglesOffToKeepThe": "",
|
||||
"manualOnlyNeverAutoCheck": "",
|
||||
"onStartupOncePerServerLaunch": "",
|
||||
|
||||
Reference in New Issue
Block a user