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
37 changes: 29 additions & 8 deletions src/signals/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
PullRequestRecord,
PullRequestReviewRecord,
RecentMergedPullRequestRecord,
RegistryRepoConfig,
RegistrySnapshot,
RepoLabelRecord,
RepoSyncStateRecord,
Expand Down Expand Up @@ -3019,6 +3020,33 @@ export function buildCollisionEdges(report: CollisionReport): CollisionEdgeRecor
});
}

// All comparable RegistryRepoConfig fields, rendered to a stable string for diffing.
// Mirrors REGISTRY_DRIFT_COMPARABLE_FIELDS in upstream/ruleset.ts so the live change
// report and the drift comparator cannot diverge as config fields are added — every
// scoring-relevant field (fixed_base_score, default_label_multiplier, eligibility_mode)
// is covered, not just the emission/lane subset.
const REGISTRY_CHANGE_FIELDS: Array<{ label: string; render: (config: RegistryRepoConfig) => string }> = [
{ label: "emission_share", render: (config) => String(config.emissionShare) },
{ label: "issue_discovery_share", render: (config) => String(config.issueDiscoveryShare) },
{ label: "maintainer_cut", render: (config) => String(config.maintainerCut) },
{ label: "fixed_base_score", render: (config) => (config.fixedBaseScore ?? null) === null ? "none" : String(config.fixedBaseScore) },
{ label: "default_label_multiplier", render: (config) => (config.defaultLabelMultiplier ?? null) === null ? "none" : String(config.defaultLabelMultiplier) },
{ label: "eligibility_mode", render: (config) => config.eligibilityMode ?? "default" },
/* v8 ignore next -- Boolean defaulting protects older registry snapshots without trusted_label_pipeline. */
{ label: "trusted_label_pipeline", render: (config) => String(config.trustedLabelPipeline ?? false) },
{ label: "label_multipliers", render: (config) => JSON.stringify(config.labelMultipliers) },
];

function registryConfigChanges(previous: RegistryRepoConfig, current: RegistryRepoConfig): string[] {
return REGISTRY_CHANGE_FIELDS.flatMap((field) => {
const before = field.render(previous);
const after = field.render(current);
if (before === after) return [];
// labelMultipliers is an object diff; report the fact of change, not the JSON blob.
return [field.label === "label_multipliers" ? "label_multipliers changed" : `${field.label} ${before} -> ${after}`];
});
}

export function buildRegistryChangeReport(snapshots: RegistrySnapshot[]): RegistryChangeReport {
const [current, previous] = snapshots;
if (!current) {
Expand Down Expand Up @@ -3048,14 +3076,7 @@ export function buildRegistryChangeReport(snapshots: RegistrySnapshot[]): Regist
.flatMap(([repoFullName, repo]) => {
const old = previousByRepo.get(repoFullName);
if (!old) return [];
const changes = [
...(repo.emissionShare !== old.emissionShare ? [`emission_share ${old.emissionShare} -> ${repo.emissionShare}`] : []),
...(repo.issueDiscoveryShare !== old.issueDiscoveryShare ? [`issue_discovery_share ${old.issueDiscoveryShare} -> ${repo.issueDiscoveryShare}`] : []),
...(repo.maintainerCut !== old.maintainerCut ? [`maintainer_cut ${old.maintainerCut} -> ${repo.maintainerCut}`] : []),
...(JSON.stringify(repo.labelMultipliers) !== JSON.stringify(old.labelMultipliers) ? ["label_multipliers changed"] : []),
/* v8 ignore next -- Boolean defaulting protects older registry snapshots without trusted_label_pipeline. */
...(repo.trustedLabelPipeline !== old.trustedLabelPipeline ? [`trusted_label_pipeline ${old.trustedLabelPipeline ?? false} -> ${repo.trustedLabelPipeline ?? false}`] : []),
];
const changes = registryConfigChanges(old, repo);
return changes.length > 0 ? [{ repoFullName, changes }] : [];
})
.sort((left, right) => left.repoFullName.localeCompare(right.repoFullName));
Expand Down
32 changes: 31 additions & 1 deletion test/unit/signals-v2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,25 @@ describe("v2 signal builders", () => {
expect(report.changedRepos[0]?.changes).toContain("emission_share 0.01 -> 0.02");
});

it("reports changes to fixed base score, default label multiplier, and eligibility mode", () => {
// Only fixedBaseScore changes; every other compared field is identical. The base
// score override is the highest-impact registry change, so it must be surfaced.
const previous = snapshot("old", [{ repo: "JSONbored/gittensory", emissionShare: 0.02, issueDiscoveryShare: 0, labelMultipliers: {}, fixedBaseScore: 2 }]);
const current = snapshot("new", [{ repo: "JSONbored/gittensory", emissionShare: 0.02, issueDiscoveryShare: 0, labelMultipliers: {}, fixedBaseScore: 50 }]);
const report = buildRegistryChangeReport([current, previous]);
expect(report.changedRepos).toHaveLength(1);
expect(report.changedRepos[0]?.changes).toContain("fixed_base_score 2 -> 50");
expect(report.summary).toContain("1 changed");

// eligibility_mode and default_label_multiplier are tracked too.
const beforeMode = snapshot("old2", [{ repo: "JSONbored/gittensory", emissionShare: 0.02, issueDiscoveryShare: 0, labelMultipliers: {}, eligibilityMode: "branch_required", defaultLabelMultiplier: 1 }]);
const afterMode = snapshot("new2", [{ repo: "JSONbored/gittensory", emissionShare: 0.02, issueDiscoveryShare: 0, labelMultipliers: {}, eligibilityMode: "any_branch", defaultLabelMultiplier: 1.2 }]);
const modeReport = buildRegistryChangeReport([afterMode, beforeMode]);
expect(modeReport.changedRepos[0]?.changes).toEqual(
expect.arrayContaining(["eligibility_mode branch_required -> any_branch", "default_label_multiplier 1 -> 1.2"]),
);
});

it("builds repo-level maintainer packets with fallback actions", () => {
const packet = buildMaintainerPacket(repo, [], [], repo.fullName);
const busyPacket = buildMaintainerPacket(repo, issues, pullRequests, repo.fullName);
Expand Down Expand Up @@ -1518,7 +1537,18 @@ describe("v2 signal builders", () => {
});
});

function snapshot(id: string, repositories: Array<{ repo: string; emissionShare: number; issueDiscoveryShare: number; labelMultipliers: Record<string, number> }>): RegistrySnapshot {
function snapshot(
id: string,
repositories: Array<{
repo: string;
emissionShare: number;
issueDiscoveryShare: number;
labelMultipliers: Record<string, number>;
fixedBaseScore?: number | null;
defaultLabelMultiplier?: number | null;
eligibilityMode?: string | null;
}>,
): RegistrySnapshot {
return {
id,
generatedAt: "2026-05-23T00:00:00.000Z",
Expand Down