From 39932686caab3cd251e32b01056aca268fbf66d2 Mon Sep 17 00:00:00 2001 From: luciferlive112116 <291889058+luciferlive112116@users.noreply.github.com> Date: Wed, 15 Jul 2026 14:45:27 +0800 Subject: [PATCH] fix(signals): carry labelPolicy.note through focusManifestPolicyToCompilerOutput Two adapters compile the same FocusManifest into the same RepoPolicyCompilerOutput.labelPolicy shape and disagreed on one field: compileRepoPolicyCompilerOutput (repo-policy-compiler.ts) sets note: labelPolicyNote(linkedIssuePolicy), while focusManifestPolicyToCompilerOutput (onboarding-pack.ts) omitted note entirely. The adapter's own doc comment says it produces the shape expected by buildRepoOnboardingPackPreview, so the two should agree for the same manifest. Because it didn't, buildRegistrationReadiness's onboardingPackPreview.labelPolicy always rendered note: null -- silently dropping the linked-issue guidance ("Link a tracked issue before opening a pull request.") that the same manifest produces via the direct onboarding-pack API/MCP path. Export the existing labelPolicyNote helper and reuse it as-is rather than duplicating its strings; the reverse import in repo-policy-compiler.ts is type-only and erased, so no runtime cycle is introduced. No other field mapping is touched. Closes #5943 --- src/signals/onboarding-pack.ts | 2 ++ src/signals/repo-policy-compiler.ts | 5 ++- test/unit/onboarding-pack.test.ts | 39 +++++++++++++++++++++++- test/unit/registration-readiness.test.ts | 26 ++++++++++++++++ 4 files changed, 70 insertions(+), 2 deletions(-) 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({