Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
24ef60b
fix(platform): tolerate acceptance groups without review metrics
cpenaforte-clickbus Oct 5, 2026
0728f5c
feat(platform): mark repos with incomplete PR data on compare and das…
cpenaforte-clickbus Oct 5, 2026
237e3b5
fix(platform): explain the compare marker and fix pt-BR wording
cpenaforte-clickbus Oct 5, 2026
97df095
fix(platform): make the PR-degraded select opt-in, narrow fallback
cpenaforte-clickbus Oct 7, 2026
403e8b7
fix(platform): localize PR steps, dedupe them, cap the repo list
cpenaforte-clickbus Oct 7, 2026
d9c6a38
fix(platform): show the PR-data marker on mobile compare cards
cpenaforte-clickbus Oct 7, 2026
f526975
fix(platform): raise the PR-data dot contrast and fix copy
cpenaforte-clickbus Oct 8, 2026
6135669
fix(platform): show failed PR steps as text on touch and keyboard
cpenaforte-clickbus Oct 8, 2026
413c47f
refactor(platform): keep the summaries cast reason at the cast
cpenaforte-clickbus Oct 8, 2026
8273415
test(platform): pin the PR-data marker on the compare page
cpenaforte-clickbus Oct 8, 2026
49ca160
fix(platform): name open-PR metrics in the PR-data tooltip
cpenaforte-clickbus Oct 8, 2026
4321e5f
fix(platform): name PR lifecycle metrics in the PR-data tooltip
cpenaforte-clickbus Oct 8, 2026
e1bec17
fix(platform): drop the PR-data badge title that repeats its steps
cpenaforte-clickbus Oct 8, 2026
4911e6d
fix(platform): show failed PR steps under the name in both layouts
cpenaforte-clickbus Oct 8, 2026
8e83ca4
fix(platform): stop the fetch step label repeating its sentence
cpenaforte-clickbus Oct 8, 2026
4628df7
Merge pull request #293 from RocketBus/fix/pr-data-incomplete-a11y
cpenaforte-clickbus Oct 8, 2026
e6b715b
Merge pull request #292 from RocketBus/fix/pr-data-incomplete-polish
cpenaforte-clickbus Oct 8, 2026
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
62 changes: 61 additions & 1 deletion platform/lib/pr-enrichment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,68 @@ const KNOWN_STEPS: Record<PrReadStep, true> = {
*/
export function prDegradedSteps(value: unknown): PrReadStep[] {
if (!Array.isArray(value)) return [];
return value.filter(
const steps = value.filter(
(step): step is PrReadStep =>
typeof step === "string" && Object.hasOwn(KNOWN_STEPS, step),
);
// Same step twice adds nothing; keep the first occurrence's position.
return [...new Set(steps)];
}

/** The `t()` of both `useTranslation` and `getServerTranslation`. */
export type Translate = (
path: string,
params?: Record<string, string | number>,
) => string;

/** Degraded steps as a localized, comma-separated list for tooltips/notes. */
export function formatPrSteps(
steps: readonly PrReadStep[],
t: Translate,
): string {
return steps.map((step) => t(`repos.detail.prData.steps.${step}`)).join(", ");
}

/** Most repo names the dashboard note lists before collapsing the rest. */
export const MAX_LISTED_REPOS = 5;

/**
* Repo names for the dashboard note: the first `max`, then a localized
* "and N more" for the remainder, so one bad batch cannot flood the page.
*/
export function formatRepoNames(
names: readonly string[],
t: Translate,
max: number = MAX_LISTED_REPOS,
): string {
const listed = names.slice(0, max).join(", ");
const rest = names.length - max;
if (rest <= 0) return listed;
return `${listed} ${t("dashboard.prDataMoreRepos", { count: rest })}`;
}

export interface DegradedRepo {
id: string;
name: string;
steps: PrReadStep[];
}

/**
* The repos whose latest payload reports a degraded PR read, in the order of
* `repos`. Takes the payloads the dashboard already loads, so it costs no
* extra query.
*/
export function degradedReposFromPayloads(
repos: ReadonlyArray<{ id: string; name: string }>,
payloads: ReadonlyMap<string, { pr_enrichment_degraded?: unknown }>,
): DegradedRepo[] {
const degraded: DegradedRepo[] = [];
for (const repo of repos) {
const steps = prDegradedSteps(
payloads.get(repo.id)?.pr_enrichment_degraded,
);
if (steps.length > 0)
degraded.push({ id: repo.id, name: repo.name, steps });
}
return degraded;
}
24 changes: 18 additions & 6 deletions platform/lib/queries/org-summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -659,15 +659,27 @@ export function computePRHealth(
const acc = p.acceptance_by_origin;
if (acc?.HUMAN && acc.HUMAN.commits_in_prs > 0) {
const w = acc.HUMAN.commits_in_prs;
humanSPRSum += acc.HUMAN.single_pass_rate * w;
humanRoundsSum += acc.HUMAN.median_review_rounds * w;
humanWeight += w;
// The engine omits the review metrics when the reviews read degraded;
// skip the group instead of letting `undefined * w` poison the sums.
if (
typeof acc.HUMAN.single_pass_rate === "number" &&
typeof acc.HUMAN.median_review_rounds === "number"
) {
humanSPRSum += acc.HUMAN.single_pass_rate * w;
humanRoundsSum += acc.HUMAN.median_review_rounds * w;
humanWeight += w;
}
}
if (acc?.AI_ASSISTED && acc.AI_ASSISTED.commits_in_prs > 0) {
const w = acc.AI_ASSISTED.commits_in_prs;
aiSPRSum += acc.AI_ASSISTED.single_pass_rate * w;
aiRoundsSum += acc.AI_ASSISTED.median_review_rounds * w;
aiWeight += w;
if (
typeof acc.AI_ASSISTED.single_pass_rate === "number" &&
typeof acc.AI_ASSISTED.median_review_rounds === "number"
) {
aiSPRSum += acc.AI_ASSISTED.single_pass_rate * w;
aiRoundsSum += acc.AI_ASSISTED.median_review_rounds * w;
aiWeight += w;
}
}
}

Expand Down
101 changes: 88 additions & 13 deletions platform/lib/queries/temporal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
* Pure functions that take a Supabase client and return typed data.
*/

import type { SupabaseClient } from "@supabase/supabase-js";
import type { PostgrestError, SupabaseClient } from "@supabase/supabase-js";

import { prDegradedSteps } from "@/lib/pr-enrichment";
import { logQueryError } from "@/lib/queries/log-error";
import type {
TimeSeriesPoint,
Expand All @@ -16,6 +17,34 @@ import { classifyHealth } from "@/types/temporal";

const SPARKLINE_POINTS = 12;

// The columns of `repo_metric_summaries` that SUMMARY_COLUMNS selects, plus the
// optional payload key. Columns are nullable because the view aggregates over
// runs that may lack a metric.
interface SummaryRow {
repository_id: string;
runs_count: number | null;
last_run_at: string | null;
stabilization_ratio: number | null;
prev_stabilization_ratio: number | null;
revert_rate: number | null;
churn_events: number | null;
commits_total: number | null;
ai_detection_coverage_pct: number | null;
pr_merged_count: number | null;
pr_single_pass_rate: number | null;
fix_latency_median_hours: number | null;
cascade_rate: number | null;
merge_strategy: string | null;
commit_metrics_reliable: boolean | null;
// Newest first; entries can be null.
recent_stabilization: Array<number | null> | null;
// Untrusted JSON from the payload; read through `prDegradedSteps`.
pr_enrichment_degraded?: unknown;
}

const SUMMARY_COLUMNS =
"repository_id, runs_count, last_run_at, stabilization_ratio, prev_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, merge_strategy, commit_metrics_reliable, recent_stabilization";

/**
* Canonical percentage-point significance thresholds, mirrored from
* `iris/analysis/trend_delta.py` (see docs/METRICS.md): delta < PP_STABLE is
Expand Down Expand Up @@ -204,6 +233,17 @@ export async function getRepoAITimeSeries(
.filter((p) => p.ai_pct !== null && p.ai_pct > 0);
}

/**
* Whether a PostgREST error says the view has no `payload` column: PostgreSQL's
* undefined_column (42703), or a PostgREST-side error naming the column.
*/
function isMissingPayloadColumn(error: PostgrestError): boolean {
return (
error.code === "42703" ||
/payload|pr_enrichment_degraded/i.test(error.message ?? "")
);
}

/** Get summary for all repos in an org (latest + previous for delta).
*
* Uses 2 bulk queries instead of 3N+1 per-repo queries:
Expand All @@ -216,11 +256,17 @@ export async function getRepoAITimeSeries(
* window, repos whose latest run fell outside the newest-1000 slice came back
* with zero rows and rendered "0 runs" despite having metrics. The view
* returns ~one row per repo, so the result set stays well under any cap.
*
* `options.includePrDegraded` also selects `payload->pr_enrichment_degraded`.
* It is off by default because reading any key of `payload` makes the view
* aggregate the payload of every run, which only the compare table needs.
* Without it every row has `pr_degraded_steps: []`.
*/
export async function getOrgReposSummary(
supabase: SupabaseClient,
organizationId: string,
windowDays: number = DEFAULT_WINDOW_DAYS,
options: { includePrDegraded?: boolean } = {},
): Promise<RepoSummary[]> {
// Query 1: all repos
const { data: repos, error: reposError } = await supabase
Expand All @@ -233,19 +279,45 @@ export async function getOrgReposSummary(
if (!repos || repos.length === 0) return [];

// Query 2: pre-aggregated summary, one row per repo (see doc comment).
const { data: summaries, error: summariesError } = await supabase
.from("repo_metric_summaries")
.select(
"repository_id, runs_count, last_run_at, stabilization_ratio, prev_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, merge_strategy, commit_metrics_reliable, recent_stabilization",
)
.eq("organization_id", organizationId)
.eq("window_days", windowDays);
const readSummaries = async (columns: string) => {
const result = await supabase
.from("repo_metric_summaries")
.select(columns)
.eq("organization_id", organizationId)
.eq("window_days", windowDays);
// The column list is built at runtime, so supabase-js can't infer the row.
return result as unknown as {
data: SummaryRow[] | null;
error: PostgrestError | null;
};
};

let summaries: SummaryRow[] | null;
let summariesError: PostgrestError | null;
if (options.includePrDegraded) {
// Selecting one key out of `payload` marks degraded PR reads without
// shipping whole payloads. The view only has `payload` since migration
// 023; when the column is missing (older database) retry without it so the
// table stays populated, just unmarked. Any other error (a timeout, say)
// is not retried: it goes the normal error path below.
({ data: summaries, error: summariesError } = await readSummaries(
`${SUMMARY_COLUMNS}, pr_enrichment_degraded:payload->pr_enrichment_degraded`,
));
if (summariesError && isMissingPayloadColumn(summariesError)) {
logQueryError(
"getOrgReposSummary (repo_metric_summaries with payload)",
summariesError,
);
({ data: summaries, error: summariesError } =
await readSummaries(SUMMARY_COLUMNS));
}
} else {
({ data: summaries, error: summariesError } =
await readSummaries(SUMMARY_COLUMNS));
}
logQueryError("getOrgReposSummary (repo_metric_summaries)", summariesError);

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

return repos.map((repo) => {
Expand All @@ -263,7 +335,7 @@ export async function getOrgReposSummary(
const sparkline = (s?.recent_stabilization ?? [])
.slice(0, SPARKLINE_POINTS)
.reverse()
.filter((v: number | null): v is number => v !== null);
.filter((v): v is number => v !== null);

return {
id: repo.id,
Expand All @@ -282,6 +354,9 @@ export async function getOrgReposSummary(
cascade_rate: s?.cascade_rate ?? null,
merge_strategy: s?.merge_strategy ?? null,
commit_metrics_reliable: s?.commit_metrics_reliable ?? null,
pr_degraded_steps: options.includePrDegraded
? prDegradedSteps(s?.pr_enrichment_degraded)
: [],
stabilization_delta: delta,
health: classifyHealth(stabilization),
sparkline,
Expand Down
3 changes: 2 additions & 1 deletion platform/lib/queries/tool-comparison.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ export function computeToolComparison(
for (const [tool, m] of Object.entries(p.acceptance_by_tool)) {
const acc = accs.get(tool) ?? emptyAcc(tool);
const weight = m.commits_in_prs ?? 0;
if (weight > 0) {
// Omitted by the engine when the PR reviews read degraded.
if (weight > 0 && typeof m.single_pass_rate === "number") {
acc.singlePassSum += m.single_pass_rate * weight;
acc.singlePassWeight += weight;
}
Expand Down
22 changes: 20 additions & 2 deletions platform/lib/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,9 @@ export const translations = {
member: "Member",
},
dashboard: {
prDataMoreRepos: "and {count} more",
prDataIncomplete:
"PR data is incomplete in the latest run of {count} repository(ies) ({repos}); PR metrics below may be missing or skewed.",
pulse: {
totalCommits: "Total Commits",
prsMerged: "PRs Merged",
Expand Down Expand Up @@ -620,8 +623,14 @@ export const translations = {
},
prData: {
incomplete: "PR data incomplete",
steps: {
basic: "PR list",
enrichment: "commit enrichment",
reviews: "reviews",
fetch: "entire read",
},
incompleteTooltip:
"This run's PR read failed at: {steps}. Merge strategy isn't classified when enrichment fails, and flow, review and in-PR metrics may be missing or skewed.",
"This run's PR read failed at: {steps}. Merge strategy isn't classified when enrichment fails for merged PRs, and PR lifecycle (counts, time to merge, cycle time), flow, review, in-PR and open-PR metrics may be missing or skewed.",
},
dora: {
title: "DORA",
Expand Down Expand Up @@ -1831,6 +1840,9 @@ export const translations = {
member: "Membro",
},
dashboard: {
prDataMoreRepos: "e mais {count}",
prDataIncomplete:
"Os dados de PR estão incompletos na execução mais recente de {count} repositório(s) ({repos}); as métricas de PR abaixo podem faltar ou sair distorcidas.",
pulse: {
totalCommits: "Commits totais",
prsMerged: "PRs mesclados",
Expand Down Expand Up @@ -2185,8 +2197,14 @@ export const translations = {
},
prData: {
incomplete: "Dados de PR incompletos",
steps: {
basic: "listagem de PRs",
enrichment: "enriquecimento de commits",
reviews: "reviews",
fetch: "leitura inteira",
},
incompleteTooltip:
"A leitura de PRs desta execução falhou em: {steps}. A estratégia de merge não é classificada quando o enriquecimento falha, e métricas de flow, de review e de commits em PR podem faltar ou sair distorcidas.",
"A leitura de PRs desta execução falhou em: {steps}. A estratégia de merge não é classificada quando o enriquecimento dos PRs mergeados falha, e métricas de ciclo de vida de PR (contagens, tempo até o merge, cycle time), de flow, de review, de commits em PR e de PRs abertos podem faltar ou sair distorcidas.",
},
dora: {
title: "DORA",
Expand Down
Loading
Loading