Skip to content
Open
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
64 changes: 48 additions & 16 deletions platform/lib/queries/temporal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,36 +378,68 @@ export function detectChanges(
return changes;
}

/** Detect changes across all repos in an org. */
/**
* Detect changes across all repos in an org.
*
* Uses 2 bulk queries instead of an N+1 (one `metrics` query per repo,
* serialized): the `repo_metric_summaries` view already carries the latest
* and previous run's values for every repo in one pre-aggregated row (see
* 023_repo_metric_summaries_prev_values.sql), so this reads one row per
* repo instead of looping — mirroring the same fix getOrgReposSummary
* already applies against this view.
*/
export async function getOrgChangeDetections(
supabase: SupabaseClient,
organizationId: string,
windowDays: number = DEFAULT_WINDOW_DAYS,
): Promise<ChangeDetection[]> {
// Query 1: all repos (for display names — the view only has repository_id).
const { data: repos } = await supabase
.from("repositories")
.select("id, name")
.eq("organization_id", organizationId);

if (!repos) return [];
if (!repos || repos.length === 0) return [];

// Query 2: pre-aggregated latest + previous values, one row per repo.
const { data: summaries } = await supabase
.from("repo_metric_summaries")
.select(
"repository_id, runs_count, last_run_at, stabilization_ratio, prev_stabilization_ratio, revert_rate, prev_revert_rate, churn_events, prev_churn_events, ai_detection_coverage_pct, prev_ai_detection_coverage_pct, prev_created_at",
)
.eq("organization_id", organizationId)
.eq("window_days", windowDays);

const summaryByRepo = new Map<
string,
NonNullable<typeof summaries>[number]
>();
for (const row of summaries ?? []) summaryByRepo.set(row.repository_id, row);

const allChanges: ChangeDetection[] = [];

for (const repo of repos) {
const { data: runs } = await supabase
.from("metrics")
.select(
"created_at, stabilization_ratio, revert_rate, churn_events, commits_total, ai_detection_coverage_pct, pr_merged_count, pr_single_pass_rate, fix_latency_median_hours, cascade_rate",
)
.eq("repository_id", repo.id)
.eq("window_days", windowDays)
.order("created_at", { ascending: false })
.limit(2);

if (!runs || runs.length < 2) continue;

const current: TimeSeriesPoint = { date: runs[0].created_at, ...runs[0] };
const previous: TimeSeriesPoint = { date: runs[1].created_at, ...runs[1] };
const s = summaryByRepo.get(repo.id);
if (!s || s.runs_count < 2) continue;

// commits_total isn't compared by detectChanges — left null rather than
// guessed at, since the view doesn't track a "previous" value for it.
const current: TimeSeriesPoint = {
date: s.last_run_at,
stabilization_ratio: s.stabilization_ratio,
revert_rate: s.revert_rate,
churn_events: s.churn_events,
commits_total: null,
ai_detection_coverage_pct: s.ai_detection_coverage_pct,
};
const previous: TimeSeriesPoint = {
date: s.prev_created_at,
stabilization_ratio: s.prev_stabilization_ratio,
revert_rate: s.prev_revert_rate,
churn_events: s.prev_churn_events,
commits_total: null,
ai_detection_coverage_pct: s.prev_ai_detection_coverage_pct,
};

allChanges.push(...detectChanges(repo.name, repo.id, current, previous));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
-- Extends repo_metric_summaries with the previous run's values for the
-- metrics getOrgChangeDetections compares (revert_rate, churn_events,
-- ai_detection_coverage_pct) plus the previous run's timestamp.
--
-- getOrgChangeDetections used to fetch the org's repos, then loop over them
-- issuing one `metrics` query per repo (limit 2, newest-first) to compare
-- the latest run against the one before it — an N+1 that scales linearly
-- with repo count and serializes one round-trip per repo.
--
-- The view already computes prev_stabilization_ratio ([2] = second-newest)
-- for the sparkline delta arrow; extending it with the same [2] slot for
-- the other compared metrics lets getOrgChangeDetections read one
-- pre-aggregated row per repo instead of looping, mirroring the same
-- "2 bulk queries instead of N+1" fix getOrgReposSummary already uses
-- against this view (see 022_repo_metric_summaries.sql).

CREATE OR REPLACE VIEW repo_metric_summaries AS
SELECT
repository_id,
organization_id,
window_days,

count(*) AS runs_count,
max(created_at) AS last_run_at,

-- Latest run's indexed values ([1] = newest by created_at).
(array_agg(stabilization_ratio ORDER BY created_at DESC))[1] AS stabilization_ratio,
(array_agg(revert_rate ORDER BY created_at DESC))[1] AS revert_rate,
(array_agg(churn_events ORDER BY created_at DESC))[1] AS churn_events,
(array_agg(commits_total ORDER BY created_at DESC))[1] AS commits_total,
(array_agg(ai_detection_coverage_pct ORDER BY created_at DESC))[1] AS ai_detection_coverage_pct,
(array_agg(pr_merged_count ORDER BY created_at DESC))[1] AS pr_merged_count,
(array_agg(pr_single_pass_rate ORDER BY created_at DESC))[1] AS pr_single_pass_rate,
(array_agg(fix_latency_median_hours ORDER BY created_at DESC))[1] AS fix_latency_median_hours,
(array_agg(cascade_rate ORDER BY created_at DESC))[1] AS cascade_rate,
(array_agg(merge_strategy ORDER BY created_at DESC))[1] AS merge_strategy,
(array_agg(commit_metrics_reliable ORDER BY created_at DESC))[1] AS commit_metrics_reliable,

-- Previous run's values ([2] = second newest) for the change-detection
-- current-vs-previous comparison in getOrgChangeDetections.
(array_agg(created_at ORDER BY created_at DESC))[2] AS prev_created_at,
(array_agg(stabilization_ratio ORDER BY created_at DESC))[2] AS prev_stabilization_ratio,
(array_agg(revert_rate ORDER BY created_at DESC))[2] AS prev_revert_rate,
(array_agg(churn_events ORDER BY created_at DESC))[2] AS prev_churn_events,
(array_agg(ai_detection_coverage_pct ORDER BY created_at DESC))[2] AS prev_ai_detection_coverage_pct,

-- Newest-first stabilization values; the caller slices SPARKLINE_POINTS,
-- reverses to chronological, and drops nulls. 50 is more than any sparkline
-- needs while keeping the array small.
(array_agg(stabilization_ratio ORDER BY created_at DESC))[1:50] AS recent_stabilization

FROM metrics
GROUP BY repository_id, organization_id, window_days;
Loading