Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/signals/onboarding-pack.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -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,
Expand Down
5 changes: 4 additions & 1 deletion src/signals/repo-policy-compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.";
Expand Down
39 changes: 38 additions & 1 deletion test/unit/onboarding-pack.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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();
Expand Down
26 changes: 26 additions & 0 deletions test/unit/registration-readiness.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down