diff --git a/src/signals/focus-manifest.ts b/src/signals/focus-manifest.ts index 46d4a6523e..42a64524a9 100644 --- a/src/signals/focus-manifest.ts +++ b/src/signals/focus-manifest.ts @@ -685,11 +685,16 @@ export function buildFocusManifestGuidance(args: { } if (manifest.wantedPaths.length > 0 && matchedWantedPaths.length === 0 && changedPaths.length > 0) { + // Public-safety filter before interpolation (#5945) -- mirrors safeExpectations below. manifest.wantedPaths + // is maintainer-authored and never public-safety-checked upstream; without this, a public-unsafe pattern + // would leak verbatim into a contributor-facing finding. + const safeWantedPaths = manifest.wantedPaths.filter(isFocusManifestPublicSafe).slice(0, 5); + const wantedPathsDetail = safeWantedPaths.length > 0 ? ` (${safeWantedPaths.join(", ")})` : ""; findings.push({ code: "manifest_off_focus", severity: "warning", title: "Change is outside maintainer-wanted areas", - detail: `No changed path matches the maintainer-wanted patterns (${manifest.wantedPaths.slice(0, 5).join(", ")}).`, + detail: `No changed path matches the maintainer-wanted patterns${wantedPathsDetail}.`, action: "Refocus the change onto a maintainer-wanted area or explain why this out-of-focus work is needed.", }); publicNextSteps.push("Refocus onto the maintainer-wanted areas, or explain why this out-of-focus change is needed."); @@ -706,11 +711,20 @@ export function buildFocusManifestGuidance(args: { } if (manifest.preferredLabels.length > 0 && preferredLabelHits.length === 0) { + // Public-safety filter before interpolation (#5945) -- mirrors safeExpectations below. Unlike + // manifest_off_focus, this finding's ENTIRE detail is built from the label list, so a zero-safe-entries + // fallback needs its own sentence, not just a dropped parenthetical -- the title text already says the + // same thing and is a static, always-public-safe string. + const safePreferredLabels = manifest.preferredLabels.filter(isFocusManifestPublicSafe).slice(0, 5); + const preferredLabelsDetail = + safePreferredLabels.length > 0 + ? `Maintainer prefers labels: ${safePreferredLabels.join(", ")}.` + : "No maintainer-preferred label applied."; findings.push({ code: "manifest_missing_preferred_label", severity: "info", title: "No maintainer-preferred label applied", - detail: `Maintainer prefers labels: ${manifest.preferredLabels.slice(0, 5).join(", ")}.`, + detail: preferredLabelsDetail, action: "Consider applying a maintainer-preferred label so triage stays aligned.", }); publicNextSteps.push(`Consider a maintainer-preferred label (${manifest.preferredLabels.slice(0, 3).join(", ")}).`); diff --git a/test/unit/focus-manifest.test.ts b/test/unit/focus-manifest.test.ts index aa11bd7b4c..89d6ddffc3 100644 --- a/test/unit/focus-manifest.test.ts +++ b/test/unit/focus-manifest.test.ts @@ -647,6 +647,28 @@ describe("buildFocusManifestGuidance", () => { expect(missingTests?.detail).not.toContain("wallet"); }); + it("filters public-unsafe wantedPaths/preferredLabels entries out of finding details (#5945)", () => { + // manifest_off_focus and manifest_missing_preferred_label interpolate wantedPaths/preferredLabels + // directly into contributor-facing detail text -- both are maintainer-authored and must be filtered + // through isFocusManifestPublicSafe before interpolation, same as testExpectations above (#3304). + const unsafeManifest = parseFocusManifest({ wantedPaths: ["wallet-onboarding/"], preferredLabels: ["reward-tracking"] }); + const guidance = buildFocusManifestGuidance({ + manifest: unsafeManifest, + changedPaths: ["src/x.ts"], + labels: [], + linkedIssueCount: 1, + testFileCount: 1, + }); + + const offFocus = guidance.findings.find((finding) => finding.code === "manifest_off_focus"); + expect(offFocus?.detail).toBe("No changed path matches the maintainer-wanted patterns."); + expect(offFocus?.detail).not.toContain("wallet"); + + const missingLabel = guidance.findings.find((finding) => finding.code === "manifest_missing_preferred_label"); + expect(missingLabel?.detail).toBe("No maintainer-preferred label applied."); + expect(missingLabel?.detail).not.toContain("reward"); + }); + it("treats passing validation as satisfying test expectations", () => { const guidance = buildFocusManifestGuidance({ manifest: wanted, changedPaths: ["src/x.ts"], linkedIssueCount: 1, testFileCount: 0, passedValidationCount: 2 }); expect(guidance.findings.some((finding) => finding.code === "manifest_missing_tests")).toBe(false); diff --git a/test/unit/local-branch.test.ts b/test/unit/local-branch.test.ts index 68ef2c6c8e..b63f88c66d 100644 --- a/test/unit/local-branch.test.ts +++ b/test/unit/local-branch.test.ts @@ -1660,6 +1660,40 @@ describe("local branch analysis", () => { expect(JSON.stringify(analysis.prPacket)).not.toMatch(/reward|score|wallet|hotkey|farming|payout|ranking|trust score/i); }); + it("does not leak public-unsafe wantedPaths/preferredLabels through localFindings (#5945)", () => { + // manifestGuidance.findings is mapped verbatim into localFindings (the actual /v1 API + MCP + // exposure path), so a maintainer-authored public-unsafe wantedPaths/preferredLabels entry must + // already be filtered out of the finding detail upstream in buildFocusManifestGuidance -- this + // closes the contributor-facing exposure path, not just the guidance-builder unit above. + const analysis = buildLocalBranchAnalysis({ + input: { + login: "oktofeesh1", + repoFullName: "entrius/allways-ui", + branchName: "fix-cache", + body: "Fixes #7", + changedFiles: [{ path: "src/cache.ts", additions: 4, deletions: 0, status: "modified" }], + focusManifest: { + source: "repo_file", + wantedPaths: ["wallet-onboarding/"], + preferredLabels: ["reward-tracking"], + }, + }, + repo, + issues: [{ repoFullName: repo.fullName, number: 7, title: "Cache refresh", state: "open", labels: [], linkedPrs: [] }], + pullRequests: [], + profile, + outcomeHistory, + scoringSnapshot, + scoringProfile, + }); + + const offFocus = analysis.localFindings.find((finding) => finding.code === "manifest_off_focus"); + expect(offFocus?.detail).not.toContain("wallet"); + const missingLabel = analysis.localFindings.find((finding) => finding.code === "manifest_missing_preferred_label"); + expect(missingLabel?.detail).not.toContain("reward"); + expect(JSON.stringify(analysis.localFindings)).not.toMatch(/wallet|reward/i); + }); + it("ignores a malformed focus manifest without breaking analysis", () => { const analysis = buildLocalBranchAnalysis({ input: {