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
15 changes: 13 additions & 2 deletions platform/lib/queries/org-summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1088,6 +1088,18 @@ function diff(current: number | null, previous: number | null): number | null {
// computeHyperEngineers β€” aggregate across repos, deduplicate by name
// ---------------------------------------------------------------------------

/**
* The "hyper engineer" threshold, shared so the org-wide aggregate here and
* the repo-detail page's per-author badge never drift apart by having the
* same comparison hand-copied in two places.
*/
export function isHyperEngineer(author: {
high_velocity_weeks: number;
ai_commit_pct: number;
}): boolean {
return author.high_velocity_weeks > 0 || author.ai_commit_pct >= 80;
}

export function computeHyperEngineers(
payloads: Map<string, ReportMetrics>,
userMap: Map<string, { name: string; github?: string }>,
Expand All @@ -1110,8 +1122,7 @@ export function computeHyperEngineers(

for (const a of av.authors) {
const key = a.name.toLowerCase();
const isHyper = a.high_velocity_weeks > 0 || a.ai_commit_pct >= 80;
if (!isHyper) continue;
if (!isHyperEngineer(a)) continue;

const existing = authors.get(key) ?? {
name: a.name,
Expand Down
24 changes: 20 additions & 4 deletions platform/lib/tenant.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { cache } from "react";

import { headers } from "next/headers";

import { debugDatabase, logError } from "./debug";
Expand Down Expand Up @@ -68,12 +70,24 @@ export async function getTenantFromRequest(): Promise<TenantContext> {
}

/**
* Check if user has access to tenant
* Check if user has access to tenant.
*
* Wrapped in React's `cache()` so calling this again with the same
* (tenant, userId) elsewhere in the same request β€” e.g. a page that needs
* the caller's role for a permission check β€” dedupes to the layout's
* existing call instead of re-running the same two queries. Also returns
* the resolved org id/name so a page needing "org + role" never has to
* hand-roll its own lookup of data the layout already fetched.
*/
export async function checkTenantAccess(
export const checkTenantAccess = cache(async function checkTenantAccess(
tenant: string,
userId: string,
): Promise<{ hasAccess: boolean; role?: string }> {
): Promise<{
hasAccess: boolean;
role?: string;
orgId?: string;
orgName?: string;
}> {
try {
debugDatabase("Checking tenant access", { tenant, userId });

Expand Down Expand Up @@ -143,9 +157,11 @@ export async function checkTenantAccess(
return {
hasAccess: true,
role: membership.role,
orgId: org.id,
orgName: org.name,
};
} catch (error) {
logError(error, "checkTenantAccess");
return { hasAccess: false };
}
}
});
22 changes: 18 additions & 4 deletions platform/lib/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,8 @@ export const translations = {
tightCouplingTitle: "Tight coupling",
tightCouplingReason:
"{fileA} and {fileB} change together {rate}% of the time ({count} joint changes). Decoupling would reduce rework cost across the area.",
tightCouplingLowSample:
"Based on only {count} joint changes β€” treat as a hypothesis, not a confirmed pattern.",
fixMagnetTitle: "{origin} attracts fixes",
fixMagnetReason:
"Commits from {origin} represent {codeShare}% of changes but {fixShare}% of fixes ({disp}Γ— the baseline, {count} fixes in window). Review patterns may need adjustment for this origin.",
Expand All @@ -492,10 +494,13 @@ export const translations = {
detected: "AI adoption detected on {date} ({count} AI commits)",
hypothesisNote:
"Deltas compare pre-adoption and post-adoption windows. Correlation, not causation β€” other changes may have overlapped.",
// No "insufficient" entry: AdoptionTimelineCard returns its own
// dedicated empty state for that confidence level before ever
// reaching the badge that reads this key (see adoption.insufficient
// below for that copy) β€” a badge label here would be dead code.
confidence: {
clear: "Clear",
sparse: "Sparse",
insufficient: "Insufficient",
},
columns: {
metric: "Metric",
Expand Down Expand Up @@ -575,7 +580,9 @@ export const translations = {
},
repos: {
title: "Repositories",
subtitle: "{count} repositories in {org}",
subtitle: "{count} {noun} in {org}",
repositorySingular: "repository",
repositoryPlural: "repositories",
deleteButton: "Delete repository",
deleteDialog: {
title: "Delete Repository",
Expand Down Expand Up @@ -610,6 +617,8 @@ export const translations = {
"Deployment metrics scoped to this repository over the last {days} days β€” live from Datadog as of now. The rest of this page reflects the last analysis run, which may be older.",
lowSample:
"Based on only {actual} evaluated deploys (below the {threshold} this page treats as a stable read) β€” treat these four as directional, not precise.",
empty:
"No deployment data in this window β€” either no Datadog integration is connected, or this repository had zero deploys in the selected period.",
incidentDisclaimer:
"MTTR by incident isn't shown per-repo β€” Datadog failure events don't carry repository attribution, so any per-repo number would be a misleading copy of the org-wide one. See the dashboard for the incident-level view.",
},
Expand Down Expand Up @@ -1878,6 +1887,8 @@ export const translations = {
tightCouplingTitle: "Acoplamento alto",
tightCouplingReason:
"{fileA} e {fileB} mudam juntos {rate}% das vezes ({count} mudanΓ§as conjuntas). Desacoplar reduziria o custo de retrabalho na Γ‘rea.",
tightCouplingLowSample:
"Baseado em apenas {count} mudanΓ§as conjuntas β€” trate como hipΓ³tese, nΓ£o como padrΓ£o confirmado.",
fixMagnetTitle: "{origin} atrai fixes",
fixMagnetReason:
"Commits de {origin} representam {codeShare}% das mudanΓ§as mas {fixShare}% dos fixes ({disp}Γ— o baseline, {count} fixes na janela). Os padrΓ΅es de review podem precisar de ajuste para essa origem.",
Expand All @@ -1903,7 +1914,6 @@ export const translations = {
confidence: {
clear: "Claro",
sparse: "Esparso",
insufficient: "Insuficiente",
},
columns: {
metric: "MΓ©trica",
Expand Down Expand Up @@ -1983,7 +1993,9 @@ export const translations = {
},
repos: {
title: "RepositΓ³rios",
subtitle: "{count} repositΓ³rios em {org}",
subtitle: "{count} {noun} em {org}",
repositorySingular: "repositΓ³rio",
repositoryPlural: "repositΓ³rios",
deleteButton: "Excluir repositΓ³rio",
deleteDialog: {
title: "Excluir RepositΓ³rio",
Expand Down Expand Up @@ -2019,6 +2031,8 @@ export const translations = {
"MΓ©tricas de deploy deste repositΓ³rio nos ΓΊltimos {days} dias β€” direto do Datadog, a partir de agora. O resto desta pΓ‘gina reflete a ΓΊltima anΓ‘lise, que pode ser mais antiga.",
lowSample:
"Baseado em apenas {actual} deploys avaliados (abaixo dos {threshold} que esta pΓ‘gina considera uma leitura estΓ‘vel) β€” trate esses quatro nΓΊmeros como direcionais, nΓ£o precisos.",
empty:
"Sem dados de deploy nessa janela β€” ou nΓ£o hΓ‘ integraΓ§Γ£o com o Datadog conectada, ou esse repositΓ³rio teve zero deploys no perΓ­odo selecionado.",
incidentDisclaimer:
"MTTR por incidente nΓ£o aparece per-repo β€” os eventos de falha do Datadog nΓ£o carregam atribuiΓ§Γ£o de repositΓ³rio, entΓ£o qualquer nΓΊmero per-repo seria uma cΓ³pia enganosa do org-wide. Veja a visΓ£o de incidentes no dashboard.",
},
Expand Down
10 changes: 7 additions & 3 deletions platform/src/app/[tenant]/repos/[repoName]/charts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,14 @@ function DistributionBar({
labels: Record<string, string>;
colors: Record<string, string>;
}) {
const total = Object.values(data).reduce((a, b) => a + b, 0);
if (total === 0) return null;

const order = Object.keys(labels);
// Sum only over the keys this bar actually renders, matching
// FlowEfficiencyCard's totalHours below β€” if the engine ever adds an
// intent/origin value with no matching label here, that bucket's count
// would otherwise inflate `total` while never appearing in the bar,
// making the rendered segments silently sum to less than 100%.
const total = order.reduce((sum, key) => sum + (data[key] ?? 0), 0);
if (total === 0) return null;

return (
<div className="space-y-2">
Expand Down
42 changes: 39 additions & 3 deletions platform/src/app/[tenant]/repos/[repoName]/dora-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,54 @@ import { Activity } from "lucide-react";

import { MetricCard } from "@/components/charts/MetricCard";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent } from "@/components/ui/card";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { useTranslation } from "@/hooks/useTranslation";
import { MIN_EVALUATED_FOR_KPIS } from "@/lib/queries/dora";
import type { RepoDORA } from "@/types/org-summary";

interface Props {
data: RepoDORA;
data: RepoDORA | null;
/** Needed for the empty state's subtitle β€” `data` has no windowDays when null. */
windowDays: number;
}

export function DORARepoCard({ data }: Props) {
export function DORARepoCard({ data, windowDays }: Props) {
const { t } = useTranslation();

if (!data) {
// Investment Hotspots and Adoption Timeline both always render an
// explanatory card when they have nothing to show β€” this section used
// to just vanish instead, leaving no indication of why (no Datadog
// integration? zero deploys in the window? disabled?).
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
{t("repos.detail.dora.title")}
<Badge variant="outline" className="border-primary/40 text-primary">
<Activity className="mr-1 size-3" />
{t("dashboard.dora.sourceBadge")}
</Badge>
</CardTitle>
<CardDescription>
{t("repos.detail.dora.subtitle", { days: windowDays })}
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
{t("repos.detail.dora.empty")}
</p>
</CardContent>
</Card>
);
}

const evaluatedDeploys =
data.deploymentsTotal - data.deploymentsPendingEvaluation;
const lowSample = evaluatedDeploys < MIN_EVALUATED_FOR_KPIS;
Expand Down
14 changes: 14 additions & 0 deletions platform/src/app/[tenant]/repos/[repoName]/investment-hotspots.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ function HotspotRow({ hotspot }: { hotspot: InvestmentHotspot }) {
}

if (hotspot.kind === "tight_coupling") {
// The engine's own floor for even surfacing a coupling hotspot is 3
// joint changes (COUPLING_MIN_OCCURRENCES in invest-here.ts) β€” right at
// that floor, "90% coupled" is 3-for-3, not a real trend. Below this
// (still low but arbitrary) bar, say so instead of badging it exactly
// like a hotspot backed by dozens of occurrences.
const isLowSample = hotspot.coOccurrences < 5;

return (
<div className="flex items-start gap-3 border-b border-border px-4 py-3 last:border-0">
<Link2 className="mt-0.5 size-4 shrink-0 text-muted-foreground" />
Expand All @@ -95,6 +102,13 @@ function HotspotRow({ hotspot }: { hotspot: InvestmentHotspot }) {
count: hotspot.coOccurrences,
})}
</p>
{isLowSample && (
<p className="text-xs text-signal-yellow">
{t("investHere.tightCouplingLowSample", {
count: hotspot.coOccurrences,
})}
</p>
)}
</div>
</div>
);
Expand Down
6 changes: 4 additions & 2 deletions platform/src/app/[tenant]/repos/[repoName]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { authOptions } from "@/lib/auth";
import { extractAdoptionSummary } from "@/lib/queries/adoption-timeline";
import { computeRepoDORA } from "@/lib/queries/dora";
import { computeInvestmentHotspots } from "@/lib/queries/invest-here";
import { isHyperEngineer } from "@/lib/queries/org-summary";
import {
getAvailableWindowDays,
resolveWindowDays,
Expand Down Expand Up @@ -189,7 +190,7 @@ export default async function RepoDetailPage({
}) ?? {};
const hyperEngineers = new Set(
(authorVelocity.authors ?? [])
.filter((a) => a.high_velocity_weeks > 0 || a.ai_commit_pct >= 80)
.filter((a) => isHyperEngineer(a))
.map((a) => a.name),
);

Expand Down Expand Up @@ -297,6 +298,7 @@ export default async function RepoDetailPage({
: "\u2014"
}
delta={revertDelta}
deltaDecimals={1}
invertDelta
// Same defaulting issue as stabilization above, but toward 0.0%.
hint={
Expand Down Expand Up @@ -327,7 +329,7 @@ export default async function RepoDetailPage({
aiImpact={aiImpact}
/>

{repoDORA && <DORARepoCard data={repoDORA} />}
<DORARepoCard data={repoDORA} windowDays={windowDays} />

<AdoptionTimelineCard summary={adoptionSummary} compact />

Expand Down
37 changes: 18 additions & 19 deletions platform/src/app/[tenant]/repos/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from "@/lib/queries/temporal";
import { getServerTranslation } from "@/lib/server-translation";
import { supabaseAdmin } from "@/lib/supabase";
import { checkTenantAccess } from "@/lib/tenant";

export default async function ReposPage({
params,
Expand All @@ -29,36 +30,30 @@ export default async function ReposPage({
const { window: windowParam } = await searchParams;
const { t } = await getServerTranslation();

const { data: org } = await supabaseAdmin
.from("organizations")
.select("id, name")
.eq("slug", tenant)
.single();

if (!org) notFound();

const { data: membership } = await supabaseAdmin
.from("organization_members")
.select("role")
.eq("user_id", session.user.id)
.eq("organization_id", org.id)
.single();
// Deduped (React cache()) against the [tenant] layout's own call for the
// same (tenant, userId) β€” this used to re-run its own org-by-slug and
// membership-by-user queries here, duplicating exactly what the layout
// had already fetched to decide whether to render this page at all.
const { hasAccess, role, orgId, orgName } = await checkTenantAccess(
tenant,
session.user.id,
);
if (!hasAccess || !orgId || !orgName) notFound();

const role = membership?.role as "owner" | "admin" | "member" | undefined;
const canDelete = role === "owner" || role === "admin";

// Analysis window (issue #80): resolve to a window the org actually has
// data for, instead of defaulting to 90d and showing every repo as "0
// runs" when the org ingests under a different window.
const availableWindows = await getAvailableWindowDays(supabaseAdmin, org.id);
const availableWindows = await getAvailableWindowDays(supabaseAdmin, orgId);
const windowDays = resolveWindowDays(
parseWindowParam(windowParam),
availableWindows,
);

const repoSummaries = await getOrgReposSummary(
supabaseAdmin,
org.id,
orgId,
windowDays,
);

Expand All @@ -70,7 +65,11 @@ export default async function ReposPage({
<p className="text-sm text-muted-foreground">
{t("repos.subtitle", {
count: repoSummaries.length,
org: org.name,
org: orgName,
noun:
repoSummaries.length === 1
? t("repos.repositorySingular")
: t("repos.repositoryPlural"),
})}
</p>
</div>
Expand All @@ -80,7 +79,7 @@ export default async function ReposPage({
<RepoList
repos={repoSummaries}
orgSlug={tenant}
organizationId={org.id}
organizationId={orgId}
canDelete={canDelete}
showSearch
/>
Expand Down
Loading
Loading