From 686d8b9b9b4093f6567947a1d0c4cc3ee8fff8de Mon Sep 17 00:00:00 2001 From: ghost <49853598+JSONbored@users.noreply.github.com> Date: Tue, 9 Jun 2026 11:17:13 -0600 Subject: [PATCH] fix(registry): preserve legacy drift affected repo counts --- src/upstream/ruleset.ts | 18 +++++++++++---- test/unit/upstream-ruleset.test.ts | 35 ++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/src/upstream/ruleset.ts b/src/upstream/ruleset.ts index 9bc4f88258..8eaff92307 100644 --- a/src/upstream/ruleset.ts +++ b/src/upstream/ruleset.ts @@ -609,6 +609,7 @@ type RulesetRegistryRepo = RulesetPayload["registry"]["repositories"][number]; type RegistryHyperparameterDriftPayload = RegistryHyperparameterDriftSummary & { events: RegistryHyperparameterDriftEvent[]; affectedRepos: string[]; + unidentifiedAffectedRepoCount: number; }; function uniqueRepoNames(events: RegistryHyperparameterDriftEvent[]): string[] { @@ -665,6 +666,7 @@ function buildRegistryHyperparameterDrift(previous: RulesetRegistryRepo[], curre ...summarizeRegistryHyperparameterDriftEvents(events), events: capped, affectedRepos: uniqueRepoNames(events), + unidentifiedAffectedRepoCount: 0, omittedEvents: Math.max(events.length - capped.length, 0), }; } @@ -717,14 +719,18 @@ function summarizeRegistryHyperparameterDriftReports(reports: UpstreamDriftRepor const payloads = reports.map((report) => readRegistryHyperparameterDriftPayload(report.payload.registryHyperparameterDrift)); const events = payloads.flatMap((payload) => payload.events); const fallbackSummary = summarizeRegistryHyperparameterDriftEvents(events); + const affectedRepoNames = new Set(payloads.flatMap((payload) => payload.affectedRepos)); + const unidentifiedAffectedRepoCount = sum(payloads.map((payload) => payload.unidentifiedAffectedRepoCount)); return { totalEvents: sum(payloads.map((payload) => payload.totalEvents)) || fallbackSummary.totalEvents, omittedEvents: sum(payloads.map((payload) => payload.omittedEvents)), highImpactCount: sum(payloads.map((payload) => payload.highImpactCount)) || fallbackSummary.highImpactCount, // Distinct repos across reports, not the sum of per-report unique counts -- a repo affected in // several open reports must be counted once. Union the pre-cap repo lists (same approach as - // affectedFields/affectedSurfaces below). - affectedRepoCount: new Set(payloads.flatMap((payload) => payload.affectedRepos)).size || fallbackSummary.affectedRepoCount, + // affectedFields/affectedSurfaces below). Legacy capped reports may only identify the first + // affected repos in `events`, so add any stored count above the identifiable repo list as + // unidentified repos to avoid underreporting their scale. + affectedRepoCount: affectedRepoNames.size + unidentifiedAffectedRepoCount || fallbackSummary.affectedRepoCount, affectedFields: uniqueSorted(payloads.flatMap((payload) => payload.affectedFields), REGISTRY_DRIFT_FIELD_ORDER), affectedSurfaces: uniqueSorted( payloads.flatMap((payload) => payload.affectedSurfaces), @@ -741,17 +747,20 @@ function readRegistryHyperparameterDriftPayload(value: JsonValue | undefined): R const affectedFields = arrayPayload(payload.affectedFields).flatMap(readRegistryHyperparameterDriftField); const affectedSurfaces = arrayPayload(payload.affectedSurfaces).flatMap(readRegistryDriftSurface); const affectedRepos = arrayPayload(payload.affectedRepos).filter((entry): entry is string => typeof entry === "string"); + const derivedAffectedRepos = affectedRepos.length > 0 ? affectedRepos : uniqueRepoNames(events); + const affectedRepoCount = numberPayload(payload.affectedRepoCount) ?? fallback.affectedRepoCount; return { events, totalEvents: numberPayload(payload.totalEvents) ?? fallback.totalEvents, omittedEvents: numberPayload(payload.omittedEvents) ?? fallback.omittedEvents, highImpactCount: numberPayload(payload.highImpactCount) ?? fallback.highImpactCount, - affectedRepoCount: numberPayload(payload.affectedRepoCount) ?? fallback.affectedRepoCount, + affectedRepoCount, affectedFields: affectedFields.length > 0 ? affectedFields : fallback.affectedFields, affectedSurfaces: affectedSurfaces.length > 0 ? affectedSurfaces : fallback.affectedSurfaces, // Legacy payloads predate `affectedRepos`; derive it from the stored (capped) events so the // reports aggregator can still union repos rather than fall back to summing. - affectedRepos: affectedRepos.length > 0 ? affectedRepos : uniqueRepoNames(events), + affectedRepos: derivedAffectedRepos, + unidentifiedAffectedRepoCount: derivedAffectedRepos.length > 0 ? Math.max(affectedRepoCount - derivedAffectedRepos.length, 0) : 0, }; } @@ -785,6 +794,7 @@ function emptyRegistryHyperparameterDriftPayload(): RegistryHyperparameterDriftP affectedFields: [], affectedSurfaces: [], affectedRepos: [], + unidentifiedAffectedRepoCount: 0, }; } diff --git a/test/unit/upstream-ruleset.test.ts b/test/unit/upstream-ruleset.test.ts index 0a06630953..5e9b17ab87 100644 --- a/test/unit/upstream-ruleset.test.ts +++ b/test/unit/upstream-ruleset.test.ts @@ -641,6 +641,41 @@ describe("upstream ruleset drift tracking", () => { expect(status.registryHyperparameterDrift.affectedRepoCount).toBe(4); }); + it("preserves stored affected repo counts for legacy capped registry drift reports", async () => { + const env = createTestEnv(); + await persistUpstreamRulesetSnapshot(env, ruleset("current", "current-hash", "pending_saturation_model", 1, 0.01, new Date().toISOString())); + const driftEvent = (repoFullName: string) => ({ + repoFullName, + field: "maintainerCut", + previous: 0.1, + current: 0.2, + severity: "high", + affectedSurfaces: ["maintainer_economics"], + summary: `${repoFullName} maintainerCut changed`, + }); + + await upsertUpstreamDriftReport( + env, + driftReport("legacy-capped-registry-drift", { + affectedAreas: ["registry"], + payload: { + registryHyperparameterDrift: { + totalEvents: 150, + omittedEvents: 50, + highImpactCount: 150, + affectedRepoCount: 150, + affectedFields: ["maintainerCut"], + affectedSurfaces: ["maintainer_economics"], + events: Array.from({ length: 100 }, (_, index) => driftEvent(`owner/repo-${String(index).padStart(3, "0")}`)), + }, + }, + }), + ); + + const status = await loadUpstreamStatus(env); + expect(status.registryHyperparameterDrift.affectedRepoCount).toBe(150); + }); + it("builds low-severity source drift reports from legacy or partial ruleset payloads", async () => { const previous = { ...ruleset("legacy-previous", "legacy-previous-hash", "pending_saturation_model", 1, 0.01, "2026-05-30T00:00:00.000Z"),