diff --git a/src/signals/onboarding-pack.ts b/src/signals/onboarding-pack.ts index c36fb904f1..4d7b34ed95 100644 --- a/src/signals/onboarding-pack.ts +++ b/src/signals/onboarding-pack.ts @@ -1,4 +1,5 @@ import { isFocusManifestPublicSafe, type FocusManifestPolicy } from "./focus-manifest"; +import { labelPolicyNote } from "./repo-policy-compiler"; import { nowIso } from "../utils/json"; export type RepoPolicyContributionLane = { @@ -99,6 +100,7 @@ export function focusManifestPolicyToCompilerOutput(policy: FocusManifestPolicy) preferredLabels: policy.publicSafe.labelPolicy.preferredLabels, requiredLabels: [], discouragedLabels: [], + note: labelPolicyNote(policy.publicSafe.validation.linkedIssuePolicy), }, validationExpectations: policy.publicSafe.validation.expectations, readinessWarnings: policy.publicSafe.readinessWarnings, diff --git a/src/signals/repo-policy-compiler.ts b/src/signals/repo-policy-compiler.ts index 3de715f1f7..78c96a6134 100644 --- a/src/signals/repo-policy-compiler.ts +++ b/src/signals/repo-policy-compiler.ts @@ -112,7 +112,10 @@ function issueDiscoverySummary(preference: FocusManifestLanePreference, summary: return "Issue discovery is optional; confirm maintainer scope before filing new issues."; } -function labelPolicyNote(linkedIssuePolicy: string): string { +/** Shared by {@link focusManifestPolicyToCompilerOutput} (onboarding-pack.ts) so both adapters compile the same + * manifest to the same `labelPolicy.note` (#5943). The reverse import there is type-only and erased, so this + * export introduces no runtime cycle. */ +export function labelPolicyNote(linkedIssuePolicy: string): string { if (linkedIssuePolicy === "required") return "Link a tracked issue before opening a pull request."; if (linkedIssuePolicy === "preferred") return "Link a tracked issue when one exists."; return "Use labels to explain accepted scope, not to promise outcomes."; diff --git a/test/unit/onboarding-pack.test.ts b/test/unit/onboarding-pack.test.ts index a4d6be41d5..2d8f90f38c 100644 --- a/test/unit/onboarding-pack.test.ts +++ b/test/unit/onboarding-pack.test.ts @@ -5,10 +5,11 @@ import { } from "../../src/services/repo-onboarding-pack"; import { createTestEnv } from "../helpers/d1"; import { upsertRepositoryFromGitHub } from "../../src/db/repositories"; -import { parseFocusManifestContent } from "../../src/signals/focus-manifest"; +import { compileFocusManifestPolicy, parseFocusManifest, parseFocusManifestContent } from "../../src/signals/focus-manifest"; import { compileRepoPolicyCompilerOutput } from "../../src/signals/repo-policy-compiler"; import { buildRepoOnboardingPackPreview, + focusManifestPolicyToCompilerOutput, isRepoOnboardingPackPublicSafe, type RepoPolicyCompilerOutput, } from "../../src/signals/onboarding-pack"; @@ -264,6 +265,42 @@ describe("buildRepoOnboardingPackPreview", () => { }); }); +// Both adapters compile the SAME FocusManifest into the same RepoPolicyCompilerOutput.labelPolicy shape, so they +// must not disagree on any field: focusManifestPolicyToCompilerOutput omitted `note` entirely (#5943), silently +// dropping the linked-issue guidance from registration-readiness's onboardingPackPreview while the direct +// onboarding-pack API/MCP path (compileRepoPolicyCompilerOutput) kept it. +describe("focusManifestPolicyToCompilerOutput labelPolicy.note parity (#5943)", () => { + const GENERATED_AT = "2026-06-02T12:00:00.000Z"; + + it.each([ + ["required", /tracked issue before opening/i], + ["preferred", /when one exists/i], + ["optional", /accepted scope/i], + ] as const)("linkedIssuePolicy %s yields the same non-null note as compileRepoPolicyCompilerOutput", (linkedIssuePolicy, expected) => { + const manifest = parseFocusManifest({ wantedPaths: ["src/"], linkedIssuePolicy }); + const fromAdapter = focusManifestPolicyToCompilerOutput( + compileFocusManifestPolicy("owner/repo", manifest, { generatedAt: GENERATED_AT }), + ); + const fromCompiler = compileRepoPolicyCompilerOutput({ repoFullName: "owner/repo", manifest, generatedAt: GENERATED_AT }); + + expect(fromAdapter.labelPolicy?.note).toEqual(expect.any(String)); + expect(fromAdapter.labelPolicy?.note).toMatch(expected); + expect(fromAdapter.labelPolicy?.note).toBe(fromCompiler.labelPolicy?.note); + }); + + it("leaves every other labelPolicy field untouched (the fix is scoped to note)", () => { + const manifest = parseFocusManifest({ wantedPaths: ["src/"], preferredLabels: ["bug"], linkedIssuePolicy: "required" }); + const output = focusManifestPolicyToCompilerOutput(compileFocusManifestPolicy("owner/repo", manifest, { generatedAt: GENERATED_AT })); + + expect(output.labelPolicy).toEqual({ + preferredLabels: ["bug"], + requiredLabels: [], + discouragedLabels: [], + note: "Link a tracked issue before opening a pull request.", + }); + }); +}); + describe("buildRepoOnboardingPackPreviewForRepo", () => { it("returns preview_only pack for an installed repo", async () => { const env = createTestEnv(); diff --git a/test/unit/registration-readiness.test.ts b/test/unit/registration-readiness.test.ts index d82da7076c..8feebaf214 100644 --- a/test/unit/registration-readiness.test.ts +++ b/test/unit/registration-readiness.test.ts @@ -308,6 +308,32 @@ describe("buildRegistrationReadiness", () => { expect(JSON.stringify(report.onboardingPackPreview)).not.toMatch(FORBIDDEN_PUBLIC_LANGUAGE); }); + // #5943: focusManifestPolicyToCompilerOutput dropped labelPolicy.note, so this preview always rendered + // note: null — silently losing the linked-issue guidance the same manifest produces via the direct + // onboarding-pack API/MCP path (compileRepoPolicyCompilerOutput). + it("onboardingPackPreview surfaces labelPolicy.note from the manifest's linkedIssuePolicy (#5943)", () => { + const repo = repoFor("octo/note", configFor({ repo: "octo/note" })); + const readinessFor = (linkedIssuePolicy: "required" | "preferred" | "optional") => + buildRegistrationReadiness({ + repoFullName: repo.fullName, + repo, + settings: settingsFor(repo.fullName), + installation: healthyInstall, + ...signalsFor(repo, [], [], [label("bug")]), + focusManifest: parseFocusManifest({ wantedPaths: ["src/signals/"], linkedIssuePolicy }), + }); + + expect(readinessFor("required").onboardingPackPreview?.labelPolicy.note).toBe( + "Link a tracked issue before opening a pull request.", + ); + expect(readinessFor("preferred").onboardingPackPreview?.labelPolicy.note).toBe( + "Link a tracked issue when one exists.", + ); + expect(readinessFor("optional").onboardingPackPreview?.labelPolicy.note).toBe( + "Use labels to explain accepted scope, not to promise outcomes.", + ); + }); + it("onboardingPackPreview strips unsafe public notes via the policy compiler pipeline", () => { const repo = repoFor("octo/unsafe", configFor({ repo: "octo/unsafe" })); const report = buildRegistrationReadiness({