From 0c6bcb6f22fcb325fa99caa983dc08583064e0d3 Mon Sep 17 00:00:00 2001 From: web-dev0521 Date: Wed, 3 Jun 2026 00:51:26 -0600 Subject: [PATCH] feat(settings): derive contribution lanes from focus manifests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds deriveContributionLanes() to compile a FocusManifest into a standalone ContributionLanes summary that contributors and repo owners can use to plan work before touching any files. - src/signals/focus-manifest.ts: - New ContributionLanePreference type ("preferred"|"neutral"|"discouraged") - New ContributionLanes type with directPrLane, issueDiscoveryLane, preferredEntryPaths, discouragedEntryPaths, validationExpectations, issueEntryGuidance, prEntryGuidance, warnings, summary - New deriveContributionLanes(manifest) function — deterministic, no change-set required, all output fields pass isFocusManifestPublicSafe - Private helper functions for each derivation dimension, each with explicit public-safe filtering - test/unit/focus-manifest.test.ts: - 15 fixture tests in deriveContributionLanes describe block: absent manifest, wanted paths, issueDiscoveryPolicy encouraged/ discouraged, linkedIssuePolicy required/preferred, blocked paths, preferred labels, publicNotes, maintainerNotes exclusion, forbidden language filtering in testExpectations and publicNotes, source preservation, and full end-to-end fixture - Property-based sanitizer test (400 iterations, seeded LCG) verifying no forbidden language appears in any derived lane output field --- src/signals/focus-manifest.ts | 122 ++++++++++++++++++++ test/unit/focus-manifest.test.ts | 191 +++++++++++++++++++++++++++++++ 2 files changed, 313 insertions(+) diff --git a/src/signals/focus-manifest.ts b/src/signals/focus-manifest.ts index b3b02d17..3c2f495a 100644 --- a/src/signals/focus-manifest.ts +++ b/src/signals/focus-manifest.ts @@ -360,3 +360,125 @@ function summarize(manifest: FocusManifest, blocked: string[], wanted: string[]) if (manifest.wantedPaths.length > 0) return "Maintainer focus manifest: change is outside the wanted areas."; return "Maintainer focus manifest applied with no path-specific verdict."; } + +export type ContributionLanePreference = "preferred" | "neutral" | "discouraged"; + +/** + * Standalone contribution lane summary derived from a focus manifest. + * Does not require a specific change set — describes the general contribution + * policy the maintainer has declared so contributors and repo owners can plan + * their work before touching any files. + */ +export type ContributionLanes = { + present: boolean; + source: FocusManifestSource; + directPrLane: ContributionLanePreference; + issueDiscoveryLane: ContributionLanePreference; + preferredEntryPaths: string[]; + discouragedEntryPaths: string[]; + validationExpectations: string[]; + issueEntryGuidance: string[]; + prEntryGuidance: string[]; + warnings: string[]; + summary: string; +}; + +/** + * Derive contribution lanes from a focus manifest without requiring a specific + * change set. The result is deterministic, explainable, and public-safe: no + * maintainer-private notes, scoreability, reviewability, reward/risk, wallet, + * hotkey, or raw trust context appears in any output field. + */ +export function deriveContributionLanes(manifest: FocusManifest): ContributionLanes { + if (!manifest.present) { + return { + present: false, + source: manifest.source, + directPrLane: "neutral", + issueDiscoveryLane: "neutral", + preferredEntryPaths: [], + discouragedEntryPaths: [], + validationExpectations: [], + issueEntryGuidance: [], + prEntryGuidance: [], + warnings: manifest.warnings, + summary: "No maintainer focus manifest; contribution lanes are not constrained.", + }; + } + + const directPrLane = deriveDirectPrLane(manifest); + const issueDiscoveryLane = deriveIssueDiscoveryLane(manifest); + const validationExpectations = deriveValidationExpectations(manifest); + const issueEntryGuidance = deriveIssueEntryGuidance(manifest); + const prEntryGuidance = derivePrEntryGuidance(manifest); + + return { + present: true, + source: manifest.source, + directPrLane, + issueDiscoveryLane, + preferredEntryPaths: manifest.wantedPaths.filter(isFocusManifestPublicSafe), + discouragedEntryPaths: manifest.blockedPaths.filter(isFocusManifestPublicSafe), + validationExpectations, + issueEntryGuidance, + prEntryGuidance, + warnings: manifest.warnings, + summary: lanesSummary(manifest, directPrLane, issueDiscoveryLane), + }; +} + +function deriveDirectPrLane(manifest: FocusManifest): ContributionLanePreference { + if (manifest.issueDiscoveryPolicy === "encouraged") return "discouraged"; + if (manifest.wantedPaths.length > 0) return "preferred"; + return "neutral"; +} + +function deriveIssueDiscoveryLane(manifest: FocusManifest): ContributionLanePreference { + if (manifest.issueDiscoveryPolicy === "encouraged") return "preferred"; + if (manifest.issueDiscoveryPolicy === "discouraged") return "discouraged"; + return "neutral"; +} + +function deriveValidationExpectations(manifest: FocusManifest): string[] { + const expectations: string[] = []; + if (manifest.linkedIssuePolicy === "required") expectations.push("Link a tracked issue before opening a PR."); + else if (manifest.linkedIssuePolicy === "preferred") expectations.push("Link a tracked issue if one exists."); + for (const expectation of manifest.testExpectations) { + if (isFocusManifestPublicSafe(expectation)) expectations.push(expectation); + } + return expectations; +} + +function deriveIssueEntryGuidance(manifest: FocusManifest): string[] { + const guidance: string[] = []; + if (manifest.issueDiscoveryPolicy === "encouraged") guidance.push("Issue discovery reports are welcomed; search for gaps before opening a PR."); + else if (manifest.issueDiscoveryPolicy === "discouraged") guidance.push("Prefer direct fixes over new issue reports; this repo discourages issue-discovery submissions."); + if (manifest.linkedIssuePolicy === "required") guidance.push("Issues must be linked to a PR before it is opened."); + else if (manifest.linkedIssuePolicy === "preferred") guidance.push("Link an existing issue to your PR when one is available."); + return guidance; +} + +function derivePrEntryGuidance(manifest: FocusManifest): string[] { + const guidance: string[] = []; + if (manifest.wantedPaths.length > 0) { + guidance.push(`Focus changes on maintainer-wanted areas: ${manifest.wantedPaths.slice(0, 5).join(", ")}.`); + } + if (manifest.blockedPaths.length > 0) { + guidance.push(`Avoid maintainer-blocked areas: ${manifest.blockedPaths.slice(0, 5).join(", ")}.`); + } + if (manifest.preferredLabels.length > 0) { + guidance.push(`Apply a maintainer-preferred label to your PR: ${manifest.preferredLabels.slice(0, 3).join(", ")}.`); + } + for (const note of manifest.publicNotes) { + if (isFocusManifestPublicSafe(note)) guidance.push(note); + } + return [...new Set(guidance)].filter(isFocusManifestPublicSafe); +} + +function lanesSummary(manifest: FocusManifest, directPrLane: ContributionLanePreference, issueDiscoveryLane: ContributionLanePreference): string { + if (issueDiscoveryLane === "preferred" && directPrLane === "discouraged") return "Issue-discovery is the preferred contribution mode for this repo."; + if (issueDiscoveryLane === "discouraged" && manifest.wantedPaths.length > 0) return "Direct PRs focused on the wanted areas are the preferred contribution mode."; + if (directPrLane === "preferred") return "Direct PRs on the maintainer-wanted areas are preferred."; + if (issueDiscoveryLane === "discouraged") return "Direct PRs are preferred; issue-discovery submissions are discouraged."; + return "Contribution lanes are guided by the maintainer focus manifest."; +} diff --git a/test/unit/focus-manifest.test.ts b/test/unit/focus-manifest.test.ts index 0d66dd9f..2ea95dfa 100644 --- a/test/unit/focus-manifest.test.ts +++ b/test/unit/focus-manifest.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import { buildFocusManifestGuidance, + deriveContributionLanes, isFocusManifestPublicSafe, matchesManifestPath, parseFocusManifest, @@ -230,6 +231,146 @@ describe("buildFocusManifestGuidance", () => { }); }); +describe("deriveContributionLanes", () => { + it("returns neutral lanes with no constraints when no manifest is present", () => { + const lanes = deriveContributionLanes(parseFocusManifest(null)); + expect(lanes.present).toBe(false); + expect(lanes.directPrLane).toBe("neutral"); + expect(lanes.issueDiscoveryLane).toBe("neutral"); + expect(lanes.preferredEntryPaths).toEqual([]); + expect(lanes.discouragedEntryPaths).toEqual([]); + expect(lanes.validationExpectations).toEqual([]); + expect(lanes.issueEntryGuidance).toEqual([]); + expect(lanes.prEntryGuidance).toEqual([]); + expect(lanes.summary).toMatch(/not constrained/i); + }); + + it("marks direct-PR as preferred when wanted paths are declared", () => { + const lanes = deriveContributionLanes(parseFocusManifest({ wantedPaths: ["src/", "lib/"] })); + expect(lanes.present).toBe(true); + expect(lanes.directPrLane).toBe("preferred"); + expect(lanes.issueDiscoveryLane).toBe("neutral"); + expect(lanes.preferredEntryPaths).toEqual(["src/", "lib/"]); + expect(lanes.prEntryGuidance.join(" ")).toMatch(/src\//); + expect(lanes.summary).toMatch(/wanted areas are preferred/i); + }); + + it("marks issue-discovery as preferred and direct-PR as discouraged when issueDiscoveryPolicy is encouraged", () => { + const lanes = deriveContributionLanes(parseFocusManifest({ issueDiscoveryPolicy: "encouraged" })); + expect(lanes.directPrLane).toBe("discouraged"); + expect(lanes.issueDiscoveryLane).toBe("preferred"); + expect(lanes.issueEntryGuidance.join(" ")).toMatch(/welcomed|search for gaps/i); + expect(lanes.summary).toMatch(/issue.discovery is the preferred/i); + }); + + it("marks issue-discovery as discouraged when issueDiscoveryPolicy is discouraged", () => { + const lanes = deriveContributionLanes(parseFocusManifest({ wantedPaths: ["src/"], issueDiscoveryPolicy: "discouraged" })); + expect(lanes.issueDiscoveryLane).toBe("discouraged"); + expect(lanes.directPrLane).toBe("preferred"); + expect(lanes.issueEntryGuidance.join(" ")).toMatch(/prefer direct fixes|discourages/i); + expect(lanes.summary).toMatch(/wanted areas are the preferred/i); + }); + + it("surfaces validation expectations from testExpectations and linkedIssuePolicy", () => { + const lanes = deriveContributionLanes( + parseFocusManifest({ wantedPaths: ["src/"], linkedIssuePolicy: "required", testExpectations: ["unit tests for new branches", "npm run test:ci"] }), + ); + expect(lanes.validationExpectations).toContain("Link a tracked issue before opening a PR."); + expect(lanes.validationExpectations).toContain("unit tests for new branches"); + expect(lanes.validationExpectations).toContain("npm run test:ci"); + }); + + it("produces preferred validation hint for linkedIssuePolicy:preferred", () => { + const lanes = deriveContributionLanes(parseFocusManifest({ wantedPaths: ["src/"], linkedIssuePolicy: "preferred" })); + expect(lanes.validationExpectations).toContain("Link a tracked issue if one exists."); + expect(lanes.issueEntryGuidance).toContain("Link an existing issue to your PR when one is available."); + }); + + it("includes required link requirement in both validation expectations and issue entry guidance", () => { + const lanes = deriveContributionLanes(parseFocusManifest({ wantedPaths: ["src/"], linkedIssuePolicy: "required" })); + expect(lanes.validationExpectations).toContain("Link a tracked issue before opening a PR."); + expect(lanes.issueEntryGuidance).toContain("Issues must be linked to a PR before it is opened."); + }); + + it("includes blocked paths in discouragedEntryPaths and PR entry guidance", () => { + const lanes = deriveContributionLanes(parseFocusManifest({ wantedPaths: ["src/"], blockedPaths: ["migrations/", "infra/secrets.tf"] })); + expect(lanes.discouragedEntryPaths).toEqual(["migrations/", "infra/secrets.tf"]); + expect(lanes.prEntryGuidance.join(" ")).toMatch(/migrations\/.*infra\/secrets\.tf|infra\/secrets\.tf.*migrations\//); + }); + + it("includes preferred labels in PR entry guidance", () => { + const lanes = deriveContributionLanes(parseFocusManifest({ wantedPaths: ["src/"], preferredLabels: ["bug", "good first issue"] })); + expect(lanes.prEntryGuidance.join(" ")).toMatch(/bug|good first issue/); + }); + + it("includes maintainer public notes in PR entry guidance", () => { + const lanes = deriveContributionLanes(parseFocusManifest({ wantedPaths: ["src/"], publicNotes: ["Prefer small, focused PRs."] })); + expect(lanes.prEntryGuidance).toContain("Prefer small, focused PRs."); + }); + + it("excludes maintainerNotes from all output fields", () => { + const lanes = deriveContributionLanes( + parseFocusManifest({ wantedPaths: ["src/"], maintainerNotes: ["Internal: ping @owner before touching the queue processor."] }), + ); + const serialized = JSON.stringify(lanes); + expect(serialized).not.toMatch(/ping @owner/); + expect(serialized).not.toMatch(/Internal:/); + }); + + it("filters public notes containing forbidden language before including them in prEntryGuidance", () => { + const lanes = deriveContributionLanes( + parseFocusManifest({ wantedPaths: ["src/"], publicNotes: ["Maximize your reward payout", "Keep PRs focused."] }), + ); + expect(lanes.prEntryGuidance).not.toContain("Maximize your reward payout"); + expect(lanes.prEntryGuidance).toContain("Keep PRs focused."); + }); + + it("filters testExpectations containing forbidden language before including them in validationExpectations", () => { + const lanes = deriveContributionLanes( + parseFocusManifest({ wantedPaths: ["src/"], testExpectations: ["Submit your wallet seed phrase", "npm run test:ci"] }), + ); + expect(lanes.validationExpectations).not.toContain("Submit your wallet seed phrase"); + expect(lanes.validationExpectations).toContain("npm run test:ci"); + }); + + it("preserves source from the manifest", () => { + const lanes = deriveContributionLanes(parseFocusManifest({ wantedPaths: ["src/"] }, "repo_file")); + expect(lanes.source).toBe("repo_file"); + }); + + it("passes a comprehensive manifest fixture end-to-end with all fields populated", () => { + const manifest = parseFocusManifest({ + source: "repo_file", + wantedPaths: ["src/", "packages/*/lib"], + blockedPaths: ["migrations/"], + preferredLabels: ["bug", "good first issue"], + linkedIssuePolicy: "required", + testExpectations: ["unit tests for new branches"], + issueDiscoveryPolicy: "discouraged", + maintainerNotes: ["Internal: ping @owner"], + publicNotes: ["Prefer small, focused PRs."], + }); + const lanes = deriveContributionLanes(manifest); + + expect(lanes.present).toBe(true); + expect(lanes.source).toBe("repo_file"); + expect(lanes.directPrLane).toBe("preferred"); + expect(lanes.issueDiscoveryLane).toBe("discouraged"); + expect(lanes.preferredEntryPaths).toContain("src/"); + expect(lanes.discouragedEntryPaths).toContain("migrations/"); + expect(lanes.validationExpectations).toContain("Link a tracked issue before opening a PR."); + expect(lanes.validationExpectations).toContain("unit tests for new branches"); + expect(lanes.issueEntryGuidance.join(" ")).toMatch(/discourages/i); + expect(lanes.prEntryGuidance.join(" ")).toMatch(/bug|good first issue/i); + expect(lanes.prEntryGuidance).toContain("Prefer small, focused PRs."); + expect(lanes.summary).toMatch(/wanted areas/i); + + const serialized = JSON.stringify(lanes); + expect(serialized).not.toMatch(/ping @owner/); + expect(serialized).not.toMatch(/\b(wallet|hotkey|coldkey|raw trust|trust score|payout|reward|farming|private reviewability)\b/i); + }); +}); + describe("public-safe invariant", () => { it("rejects forbidden compensation/secret language", () => { expect(isFocusManifestPublicSafe("Keep PRs focused")).toBe(true); @@ -284,4 +425,54 @@ describe("public-safe invariant", () => { expect(guidance.publicNextSteps.every(isFocusManifestPublicSafe)).toBe(true); } }); + + it("never emits forbidden language in derived contribution lanes across random manifests", () => { + const stringPool = [ + "", + "src/", + "migrations/", + "Keep PRs focused.", + "Prefer small, focused PRs.", + "Maximize your reward payout", + "Internal: ping @owner", + "estimate your reward", + "paste your hotkey", + "Submit your wallet seed phrase", + "npm run test:ci", + "packages/*/lib/*.ts", + ]; + const linkedIssuePolicies = ["required", "preferred", "optional"] as const; + const issueDiscoveryPolicies = ["encouraged", "neutral", "discouraged"] as const; + + let seed = 0x8f3d4a1b; + const next = () => { + seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0; + return seed / 0x100000000; + }; + const pick = (items: readonly T[]): T => items[Math.floor(next() * items.length)] as T; + const sample = (max: number): string[] => + Array.from({ length: Math.floor(next() * (max + 1)) }, () => pick(stringPool)); + + for (let iteration = 0; iteration < 400; iteration += 1) { + const raw = { + wantedPaths: sample(4), + blockedPaths: sample(4), + preferredLabels: sample(4), + linkedIssuePolicy: pick(linkedIssuePolicies), + issueDiscoveryPolicy: pick(issueDiscoveryPolicies), + testExpectations: sample(3), + maintainerNotes: sample(4), + publicNotes: sample(4), + }; + const manifest = parseFocusManifest(raw); + const lanes = deriveContributionLanes(manifest); + const allText = [ + ...lanes.validationExpectations, + ...lanes.issueEntryGuidance, + ...lanes.prEntryGuidance, + lanes.summary, + ]; + expect(allText.every(isFocusManifestPublicSafe)).toBe(true); + } + }); });