From 24a34109fc96cf8f3a71574dbbe0e98239d78ffa Mon Sep 17 00:00:00 2001 From: philluiz2323 Date: Tue, 2 Jun 2026 02:47:34 -0700 Subject: [PATCH] Diff all comparable registry config fields in the change report --- src/signals/engine.ts | 37 ++++++++++++++++++++++++++++-------- test/unit/signals-v2.test.ts | 32 ++++++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 9 deletions(-) diff --git a/src/signals/engine.ts b/src/signals/engine.ts index 1c458cac30..18f4489bc1 100644 --- a/src/signals/engine.ts +++ b/src/signals/engine.ts @@ -11,6 +11,7 @@ import type { PullRequestRecord, PullRequestReviewRecord, RecentMergedPullRequestRecord, + RegistryRepoConfig, RegistrySnapshot, RepoLabelRecord, RepoSyncStateRecord, @@ -2989,6 +2990,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) { @@ -3018,14 +3046,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)); diff --git a/test/unit/signals-v2.test.ts b/test/unit/signals-v2.test.ts index b0fcc8164f..40fe300f9e 100644 --- a/test/unit/signals-v2.test.ts +++ b/test/unit/signals-v2.test.ts @@ -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); @@ -1451,7 +1470,18 @@ describe("v2 signal builders", () => { }); }); -function snapshot(id: string, repositories: Array<{ repo: string; emissionShare: number; issueDiscoveryShare: number; labelMultipliers: Record }>): RegistrySnapshot { +function snapshot( + id: string, + repositories: Array<{ + repo: string; + emissionShare: number; + issueDiscoveryShare: number; + labelMultipliers: Record; + fixedBaseScore?: number | null; + defaultLabelMultiplier?: number | null; + eligibilityMode?: string | null; + }>, +): RegistrySnapshot { return { id, generatedAt: "2026-05-23T00:00:00.000Z",