@sentry/junior-github 0.111.0 → 0.113.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -166,6 +166,28 @@ function githubConversationIds(body) {
166
166
  }
167
167
  return [...ids];
168
168
  }
169
+ function githubLinkedIssues(body, repositoryFullName) {
170
+ if (!body) return [];
171
+ const references = /* @__PURE__ */ new Map();
172
+ const add = (linkedRepository, rawNumber) => {
173
+ const number = Number.parseInt(rawNumber, 10);
174
+ if (!Number.isInteger(number) || number <= 0) return;
175
+ const normalizedRepository = linkedRepository.toLowerCase();
176
+ references.set(`${normalizedRepository}#${number}`, {
177
+ number,
178
+ repositoryFullName: linkedRepository
179
+ });
180
+ };
181
+ for (const match of body.matchAll(/\b([\w.-]+\/[\w.-]+)#(\d+)\b/g)) {
182
+ add(match[1] ?? "", match[2] ?? "");
183
+ }
184
+ for (const match of body.matchAll(/(?:^|[^\w/])#(\d+)\b/g)) {
185
+ add(repositoryFullName, match[1] ?? "");
186
+ }
187
+ return [...references.values()].sort(
188
+ (left, right) => left.repositoryFullName.localeCompare(right.repositoryFullName) || left.number - right.number
189
+ );
190
+ }
169
191
  function appendGitHubFooter(body, conversationId, dashboardUrl) {
170
192
  const footer = githubConversationFooter(conversationId, dashboardUrl);
171
193
  const normalizedBody = body.trimEnd();
@@ -1096,12 +1118,19 @@ function createGitHubTools(ctx) {
1096
1118
  import { createHmac, timingSafeEqual } from "crypto";
1097
1119
 
1098
1120
  // src/issue-outcomes/store.ts
1099
- import { and, eq, lte } from "drizzle-orm";
1121
+ import { and, eq, lte, sql as sql2 } from "drizzle-orm";
1100
1122
  import { z as z6 } from "zod";
1101
1123
 
1102
1124
  // src/db/schema.ts
1103
1125
  import { sql } from "drizzle-orm";
1104
- import { index, integer, pgTable, text, timestamp } from "drizzle-orm/pg-core";
1126
+ import {
1127
+ index,
1128
+ integer,
1129
+ pgTable,
1130
+ primaryKey,
1131
+ text,
1132
+ timestamp
1133
+ } from "drizzle-orm/pg-core";
1105
1134
  import { z as z5 } from "zod";
1106
1135
  var githubPullRequestStateSchema = z5.enum([
1107
1136
  "closed_unmerged",
@@ -1128,6 +1157,7 @@ var juniorGitHubIssues = pgTable(
1128
1157
  number: integer("number").notNull(),
1129
1158
  state: text("state").$type().notNull(),
1130
1159
  stateReason: text("state_reason").$type(),
1160
+ conversationIds: text("conversation_ids").array().notNull().default(sql`ARRAY[]::text[]`),
1131
1161
  openedAt: timestamp("opened_at", { withTimezone: true }).notNull(),
1132
1162
  closedAt: timestamp("closed_at", { withTimezone: true }),
1133
1163
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull()
@@ -1160,6 +1190,25 @@ var juniorGitHubPullRequests = pgTable(
1160
1190
  index("junior_github_pull_requests_open_idx").on(table.pullRequestId).where(sql`${table.state} = 'open'`)
1161
1191
  ]
1162
1192
  );
1193
+ var juniorGitHubPullRequestIssues = pgTable(
1194
+ "junior_github_pull_request_issues",
1195
+ {
1196
+ pullRequestId: text("pull_request_id").notNull().references(() => juniorGitHubPullRequests.pullRequestId, {
1197
+ onDelete: "cascade"
1198
+ }),
1199
+ issueRepositoryFullName: text("issue_repository_full_name").notNull(),
1200
+ issueNumber: integer("issue_number").notNull()
1201
+ },
1202
+ (table) => [
1203
+ primaryKey({
1204
+ columns: [
1205
+ table.pullRequestId,
1206
+ table.issueRepositoryFullName,
1207
+ table.issueNumber
1208
+ ]
1209
+ })
1210
+ ]
1211
+ );
1163
1212
 
1164
1213
  // src/issue-outcomes/store.ts
1165
1214
  var githubIssueOutcomeInputSchema = z6.object({
@@ -1174,6 +1223,10 @@ var githubIssueOutcomeInputSchema = z6.object({
1174
1223
  stateReason: githubIssueStateReasonSchema.optional(),
1175
1224
  updatedAt: z6.date()
1176
1225
  }).strict();
1226
+ var githubIssueConversationsInputSchema = z6.object({
1227
+ conversationIds: z6.array(z6.string().min(1)).min(1),
1228
+ issueId: z6.string().min(1)
1229
+ }).strict();
1177
1230
  function projectionValues(input) {
1178
1231
  return {
1179
1232
  closedAt: input.closedAt ?? null,
@@ -1204,9 +1257,27 @@ async function recordGitHubIssueOutcome(db, input) {
1204
1257
  where: lte(juniorGitHubIssues.updatedAt, outcome.updatedAt)
1205
1258
  });
1206
1259
  }
1260
+ async function recordGitHubIssueConversations(db, input) {
1261
+ const association = githubIssueConversationsInputSchema.parse(input);
1262
+ const conversationIds = sql2.join(
1263
+ association.conversationIds.map((id) => sql2`${id}`),
1264
+ sql2`, `
1265
+ );
1266
+ const updated = await db.update(juniorGitHubIssues).set({
1267
+ conversationIds: sql2`ARRAY(
1268
+ SELECT DISTINCT value
1269
+ FROM unnest(
1270
+ ${juniorGitHubIssues.conversationIds}
1271
+ || ARRAY[${conversationIds}]::text[]
1272
+ ) AS value
1273
+ ORDER BY value
1274
+ )`
1275
+ }).where(eq(juniorGitHubIssues.issueId, association.issueId)).returning({ issueId: juniorGitHubIssues.issueId });
1276
+ return updated.length > 0;
1277
+ }
1207
1278
 
1208
1279
  // src/pull-request-outcomes/store.ts
1209
- import { and as and2, eq as eq2, lte as lte2, sql as sql2 } from "drizzle-orm";
1280
+ import { and as and2, eq as eq2, lte as lte2, sql as sql3 } from "drizzle-orm";
1210
1281
  import { z as z7 } from "zod";
1211
1282
  var githubPullRequestOutcomeInputSchema = z7.object({
1212
1283
  candidateOwned: z7.boolean(),
@@ -1225,6 +1296,15 @@ var githubPullRequestConversationsInputSchema = z7.object({
1225
1296
  conversationIds: z7.array(z7.string().min(1)).min(1),
1226
1297
  pullRequestId: z7.string().min(1)
1227
1298
  }).strict();
1299
+ var githubPullRequestLinkedIssuesInputSchema = z7.object({
1300
+ linkedIssues: z7.array(
1301
+ z7.object({
1302
+ number: z7.number().int().positive(),
1303
+ repositoryFullName: z7.string().min(1)
1304
+ }).strict()
1305
+ ).min(1),
1306
+ pullRequestId: z7.string().min(1)
1307
+ }).strict();
1228
1308
  function projectionValues2(input) {
1229
1309
  return {
1230
1310
  closedAt: input.closedAt ?? null,
@@ -1269,12 +1349,12 @@ async function recordGitHubPullRequestOutcome(db, input) {
1269
1349
  }
1270
1350
  async function recordGitHubPullRequestConversations(db, input) {
1271
1351
  const association = githubPullRequestConversationsInputSchema.parse(input);
1272
- const conversationIds = sql2.join(
1273
- association.conversationIds.map((id) => sql2`${id}`),
1274
- sql2`, `
1352
+ const conversationIds = sql3.join(
1353
+ association.conversationIds.map((id) => sql3`${id}`),
1354
+ sql3`, `
1275
1355
  );
1276
1356
  const updated = await db.update(juniorGitHubPullRequests).set({
1277
- conversationIds: sql2`ARRAY(
1357
+ conversationIds: sql3`ARRAY(
1278
1358
  SELECT DISTINCT value
1279
1359
  FROM unnest(
1280
1360
  ${juniorGitHubPullRequests.conversationIds}
@@ -1287,6 +1367,30 @@ async function recordGitHubPullRequestConversations(db, input) {
1287
1367
  ).returning({ pullRequestId: juniorGitHubPullRequests.pullRequestId });
1288
1368
  return updated.length > 0;
1289
1369
  }
1370
+ async function recordGitHubPullRequestLinkedIssues(db, input) {
1371
+ const association = githubPullRequestLinkedIssuesInputSchema.parse(input);
1372
+ const inserted = await Promise.all(
1373
+ association.linkedIssues.map(
1374
+ (issue) => db.insert(juniorGitHubPullRequestIssues).select(
1375
+ db.select({
1376
+ pullRequestId: juniorGitHubPullRequests.pullRequestId,
1377
+ issueRepositoryFullName: sql3`lower(${issue.repositoryFullName})`.as(
1378
+ "issue_repository_full_name"
1379
+ ),
1380
+ issueNumber: sql3`${issue.number}`.as("issue_number")
1381
+ }).from(juniorGitHubPullRequests).where(
1382
+ eq2(
1383
+ juniorGitHubPullRequests.pullRequestId,
1384
+ association.pullRequestId
1385
+ )
1386
+ )
1387
+ ).onConflictDoNothing().returning({
1388
+ issueNumber: juniorGitHubPullRequestIssues.issueNumber
1389
+ })
1390
+ )
1391
+ );
1392
+ return inserted.some((rows) => rows.length > 0);
1393
+ }
1290
1394
 
1291
1395
  // src/webhooks/issue-outcome.ts
1292
1396
  import { z as z8 } from "zod";
@@ -1404,6 +1508,45 @@ function normalizeGitHubIssueOutcome(args) {
1404
1508
  updatedAt
1405
1509
  };
1406
1510
  }
1511
+ var canonicalIssueConversationSchema = z8.object({
1512
+ issue: z8.object({
1513
+ body: z8.string().nullable().optional(),
1514
+ id: z8.number().int().positive(),
1515
+ user: z8.object({ login: z8.string().min(1) }).strict()
1516
+ }).strict(),
1517
+ sender: z8.object({ login: z8.string().min(1) }).strict().optional()
1518
+ }).strict();
1519
+ var issueConversationSchema = z8.object({
1520
+ issue: z8.object({
1521
+ body: z8.string().nullable().optional(),
1522
+ id: z8.number().int().positive(),
1523
+ user: z8.object({ login: z8.string().min(1) }).passthrough()
1524
+ }).passthrough(),
1525
+ sender: z8.object({ login: z8.string().min(1) }).passthrough().optional()
1526
+ }).passthrough().transform(
1527
+ (provider) => canonicalIssueConversationSchema.parse({
1528
+ issue: {
1529
+ body: provider.issue.body,
1530
+ id: provider.issue.id,
1531
+ user: { login: provider.issue.user.login }
1532
+ },
1533
+ ...provider.sender ? { sender: { login: provider.sender.login } } : {}
1534
+ })
1535
+ );
1536
+ function normalizeGitHubIssueConversations(args) {
1537
+ const parsed = issueConversationSchema.safeParse(args.body);
1538
+ const botLogin = botLoginFromEmail(args.botEmail)?.toLowerCase();
1539
+ if (!parsed.success || !botLogin) return void 0;
1540
+ const authorLogin = parsed.data.issue.user.login.trim().toLowerCase();
1541
+ if (authorLogin !== botLogin) return void 0;
1542
+ const senderLogin = parsed.data.sender?.login.trim().toLowerCase();
1543
+ if (senderLogin && senderLogin !== botLogin) return void 0;
1544
+ const conversationIds = githubConversationIds(parsed.data.issue.body);
1545
+ return conversationIds.length > 0 ? {
1546
+ conversationIds,
1547
+ issueId: String(parsed.data.issue.id)
1548
+ } : void 0;
1549
+ }
1407
1550
 
1408
1551
  // src/webhooks/pull-request-outcome.ts
1409
1552
  import { z as z9 } from "zod";
@@ -1469,6 +1612,7 @@ var canonicalPullRequestConversationSchema = z9.object({
1469
1612
  id: z9.number().int().positive(),
1470
1613
  user: z9.object({ login: z9.string().min(1) }).strict()
1471
1614
  }).strict(),
1615
+ repository: z9.object({ full_name: z9.string().min(1) }).strict(),
1472
1616
  sender: z9.object({ login: z9.string().min(1) }).strict()
1473
1617
  }).strict();
1474
1618
  var pullRequestConversationSchema = z9.object({
@@ -1477,6 +1621,7 @@ var pullRequestConversationSchema = z9.object({
1477
1621
  id: z9.number().int().positive(),
1478
1622
  user: z9.object({ login: z9.string().min(1) }).passthrough()
1479
1623
  }).passthrough(),
1624
+ repository: z9.object({ full_name: z9.string().min(1) }).passthrough(),
1480
1625
  sender: z9.object({ login: z9.string().min(1) }).passthrough()
1481
1626
  }).passthrough().transform(
1482
1627
  (provider) => canonicalPullRequestConversationSchema.parse({
@@ -1485,6 +1630,7 @@ var pullRequestConversationSchema = z9.object({
1485
1630
  id: provider.pull_request.id,
1486
1631
  user: { login: provider.pull_request.user.login }
1487
1632
  },
1633
+ repository: { full_name: provider.repository.full_name },
1488
1634
  sender: { login: provider.sender.login }
1489
1635
  })
1490
1636
  );
@@ -1548,6 +1694,22 @@ function normalizeGitHubPullRequestConversations(args) {
1548
1694
  pullRequestId: String(parsed.data.pull_request.id)
1549
1695
  } : void 0;
1550
1696
  }
1697
+ function normalizeGitHubPullRequestLinkedIssues(args) {
1698
+ const parsed = pullRequestConversationSchema.safeParse(args.body);
1699
+ const botLogin = botLoginFromEmail(args.botEmail)?.toLowerCase();
1700
+ if (!parsed.success || !botLogin) return void 0;
1701
+ if (parsed.data.pull_request.user.login.trim().toLowerCase() !== botLogin || parsed.data.sender.login.trim().toLowerCase() !== botLogin) {
1702
+ return void 0;
1703
+ }
1704
+ const linkedIssues = githubLinkedIssues(
1705
+ parsed.data.pull_request.body,
1706
+ parsed.data.repository.full_name
1707
+ );
1708
+ return linkedIssues.length > 0 ? {
1709
+ linkedIssues,
1710
+ pullRequestId: String(parsed.data.pull_request.id)
1711
+ } : void 0;
1712
+ }
1551
1713
 
1552
1714
  // src/webhooks/handler.ts
1553
1715
  function verifyGitHubSignature(body, signature, secret) {
@@ -1584,7 +1746,9 @@ function createGitHubWebhookRoute(args) {
1584
1746
  const botEmail = args.botEmail();
1585
1747
  const pullRequestOutcome = eventName === "pull_request" ? normalizeGitHubPullRequestOutcome({ body, botEmail }) : void 0;
1586
1748
  const issueOutcome = eventName === "issues" ? normalizeGitHubIssueOutcome({ body, botEmail }) : void 0;
1749
+ const issueConversations = eventName === "issues" ? normalizeGitHubIssueConversations({ body, botEmail }) : void 0;
1587
1750
  const pullRequestConversations = eventName === "pull_request" ? normalizeGitHubPullRequestConversations({ body, botEmail }) : void 0;
1751
+ const pullRequestLinkedIssues = eventName === "pull_request" ? normalizeGitHubPullRequestLinkedIssues({ body, botEmail }) : void 0;
1588
1752
  if (pullRequestOutcome) {
1589
1753
  const recordedOutcome = await recordGitHubPullRequestOutcome(
1590
1754
  args.db,
@@ -1616,10 +1780,15 @@ function createGitHubWebhookRoute(args) {
1616
1780
  if (issueOutcome) {
1617
1781
  await recordGitHubIssueOutcome(args.db, issueOutcome);
1618
1782
  }
1783
+ const recordedIssueConversations = issueConversations ? await recordGitHubIssueConversations(args.db, issueConversations) : false;
1619
1784
  const recordedPullRequestConversations = pullRequestConversations ? await recordGitHubPullRequestConversations(
1620
1785
  args.db,
1621
1786
  pullRequestConversations
1622
1787
  ) : false;
1788
+ const recordedPullRequestLinkedIssues = pullRequestLinkedIssues ? await recordGitHubPullRequestLinkedIssues(
1789
+ args.db,
1790
+ pullRequestLinkedIssues
1791
+ ) : false;
1623
1792
  const resourceEvents = normalizeGitHubResourceEvents({
1624
1793
  body,
1625
1794
  deliveryId,
@@ -1628,7 +1797,7 @@ function createGitHubWebhookRoute(args) {
1628
1797
  for (const event of resourceEvents) {
1629
1798
  await args.resourceEvents.publish(event);
1630
1799
  }
1631
- if (!pullRequestOutcome && !issueOutcome && !recordedPullRequestConversations && resourceEvents.length === 0) {
1800
+ if (!pullRequestOutcome && !issueOutcome && !recordedIssueConversations && !recordedPullRequestConversations && !recordedPullRequestLinkedIssues && resourceEvents.length === 0) {
1632
1801
  return new Response("Ignored", { status: 202 });
1633
1802
  }
1634
1803
  return new Response("Accepted", { status: 202 });
@@ -1637,37 +1806,344 @@ function createGitHubWebhookRoute(args) {
1637
1806
  }
1638
1807
 
1639
1808
  // src/outcomes/report.ts
1640
- import { sql as sql3 } from "drizzle-orm";
1809
+ import { sql as sql5 } from "drizzle-orm";
1810
+ import { z as z11 } from "zod";
1811
+
1812
+ // src/outcomes/cost.ts
1813
+ import { sql as sql4 } from "drizzle-orm";
1641
1814
  import { z as z10 } from "zod";
1642
1815
  var DAY_MS = 24 * 60 * 60 * 1e3;
1643
- var WINDOWS = [7, 30, 90];
1644
- var pullRequestStatsSchema = z10.object({
1645
- closed: z10.number().int().nonnegative(),
1646
- compositionUnknown: z10.number().int().nonnegative(),
1647
- created: z10.number().int().nonnegative(),
1816
+ var costWindowSchema = z10.object({
1648
1817
  days: z10.number().int().positive(),
1649
- juniorOnly: z10.number().int().nonnegative(),
1650
- medianMergeTimeMs: z10.number().nonnegative().nullable(),
1651
- merged: z10.number().int().nonnegative(),
1652
- mixed: z10.number().int().nonnegative()
1818
+ issueCostUsd: z10.number().nonnegative().nullable(),
1819
+ medianIssueCostUsd: z10.number().nonnegative().nullable(),
1820
+ medianPullRequestCostUsd: z10.number().nonnegative().nullable(),
1821
+ pullRequestCostUsd: z10.number().nonnegative().nullable()
1822
+ }).strict().transform((row) => ({
1823
+ days: row.days,
1824
+ issueCostUsd: row.issueCostUsd ?? void 0,
1825
+ medianIssueCostUsd: row.medianIssueCostUsd ?? void 0,
1826
+ medianPullRequestCostUsd: row.medianPullRequestCostUsd ?? void 0,
1827
+ pullRequestCostUsd: row.pullRequestCostUsd ?? void 0
1828
+ }));
1829
+ var repositoryCostSchema = z10.object({
1830
+ issueCostUsd: z10.number().nonnegative().nullable(),
1831
+ pullRequestCostUsd: z10.number().nonnegative().nullable(),
1832
+ repository: z10.string().min(1)
1833
+ }).strict().transform((row) => ({
1834
+ issueCostUsd: row.issueCostUsd ?? void 0,
1835
+ pullRequestCostUsd: row.pullRequestCostUsd ?? void 0,
1836
+ repository: row.repository
1837
+ }));
1838
+ function queryRows(result) {
1839
+ if (typeof result !== "object" || result === null || !("rows" in result) || !Array.isArray(result.rows)) {
1840
+ throw new TypeError("GitHub cost query did not return rows");
1841
+ }
1842
+ return result.rows;
1843
+ }
1844
+ async function hasConversationUsageTable(db) {
1845
+ const result = await db.execute(sql4`
1846
+ SELECT to_regclass('public.junior_conversations') IS NOT NULL AS "present"
1847
+ `);
1848
+ const row = queryRows(result)[0];
1849
+ return typeof row === "object" && row !== null && "present" in row && row.present === true;
1850
+ }
1851
+ function emptyCostWindows(windows) {
1852
+ return windows.map((days) => ({
1853
+ days,
1854
+ issueCostUsd: 0,
1855
+ medianIssueCostUsd: void 0,
1856
+ medianPullRequestCostUsd: void 0,
1857
+ pullRequestCostUsd: 0
1858
+ }));
1859
+ }
1860
+ function conversationTreeCostExpr() {
1861
+ return sql4`
1862
+ coalesce((
1863
+ SELECT sum(
1864
+ CASE
1865
+ WHEN conversations.usage_json->'cost'->>'total' IS NOT NULL
1866
+ THEN (conversations.usage_json->'cost'->>'total')::double precision
1867
+ WHEN coalesce(
1868
+ conversations.usage_json->'cost'->>'input',
1869
+ conversations.usage_json->'cost'->>'output',
1870
+ conversations.usage_json->'cost'->>'cacheRead',
1871
+ conversations.usage_json->'cost'->>'cacheWrite'
1872
+ ) IS NOT NULL
1873
+ THEN coalesce((conversations.usage_json->'cost'->>'input')::double precision, 0)
1874
+ + coalesce((conversations.usage_json->'cost'->>'output')::double precision, 0)
1875
+ + coalesce((conversations.usage_json->'cost'->>'cacheRead')::double precision, 0)
1876
+ + coalesce((conversations.usage_json->'cost'->>'cacheWrite')::double precision, 0)
1877
+ ELSE 0
1878
+ END
1879
+ )
1880
+ FROM junior_conversations AS conversations
1881
+ WHERE conversations.root_conversation_id IN (
1882
+ SELECT coalesce(roots.root_conversation_id, roots.conversation_id)
1883
+ FROM junior_conversations AS roots
1884
+ WHERE roots.conversation_id = ANY (conversation_ids.ids)
1885
+ )
1886
+ ), 0::double precision)
1887
+ `;
1888
+ }
1889
+ function pullRequestConversationIdsExpr() {
1890
+ const pullRequests = juniorGitHubPullRequests;
1891
+ const issues = juniorGitHubIssues;
1892
+ return sql4`
1893
+ ARRAY(
1894
+ SELECT DISTINCT unnest(
1895
+ ${pullRequests.conversationIds}
1896
+ || coalesce((
1897
+ SELECT array_agg(DISTINCT issue_conversation_id)
1898
+ FROM ${juniorGitHubPullRequestIssues} AS issue_links
1899
+ INNER JOIN ${issues} AS linked_issues
1900
+ ON lower(linked_issues.repository_full_name) =
1901
+ issue_links.issue_repository_full_name
1902
+ AND linked_issues.number = issue_links.issue_number
1903
+ CROSS JOIN LATERAL unnest(linked_issues.conversation_ids)
1904
+ AS issue_conversation_id
1905
+ WHERE issue_links.pull_request_id = ${pullRequests.pullRequestId}
1906
+ ), ARRAY[]::text[])
1907
+ )
1908
+ )
1909
+ `;
1910
+ }
1911
+ function issueConversationIdsExpr() {
1912
+ const pullRequests = juniorGitHubPullRequests;
1913
+ const issues = juniorGitHubIssues;
1914
+ return sql4`
1915
+ ARRAY(
1916
+ SELECT DISTINCT unnest(
1917
+ ${issues.conversationIds}
1918
+ || coalesce((
1919
+ SELECT array_agg(DISTINCT pr_conversation_id)
1920
+ FROM ${juniorGitHubPullRequestIssues} AS issue_links
1921
+ INNER JOIN ${pullRequests} AS linked_prs
1922
+ ON linked_prs.pull_request_id = issue_links.pull_request_id
1923
+ CROSS JOIN LATERAL unnest(linked_prs.conversation_ids)
1924
+ AS pr_conversation_id
1925
+ WHERE issue_links.issue_repository_full_name =
1926
+ lower(${issues.repositoryFullName})
1927
+ AND issue_links.issue_number = ${issues.number}
1928
+ ), ARRAY[]::text[])
1929
+ )
1930
+ )
1931
+ `;
1932
+ }
1933
+ async function aggregateGitHubCostWindows(args) {
1934
+ if (!await hasConversationUsageTable(args.db)) {
1935
+ return emptyCostWindows(args.windows);
1936
+ }
1937
+ const starts = args.windows.map(
1938
+ (days) => [days, new Date(args.nowMs - days * DAY_MS)]
1939
+ );
1940
+ const oldestStart = starts.at(-1)[1];
1941
+ const pullRequests = juniorGitHubPullRequests;
1942
+ const issues = juniorGitHubIssues;
1943
+ const windowValues = sql4.join(
1944
+ starts.map(
1945
+ ([days, start]) => sql4`(${days}::integer, ${start}::timestamptz)`
1946
+ ),
1947
+ sql4`, `
1948
+ );
1949
+ const conversationTreeCost = conversationTreeCostExpr();
1950
+ const pullRequestConversationIds = pullRequestConversationIdsExpr();
1951
+ const issueConversationIds = issueConversationIdsExpr();
1952
+ const result = await args.db.execute(sql4`
1953
+ WITH windows(days, start_at) AS (
1954
+ VALUES ${windowValues}
1955
+ ), pull_request_entities AS (
1956
+ SELECT
1957
+ ${pullRequests.openedAt} AS opened_at,
1958
+ conversation_ids.ids AS ids,
1959
+ ${conversationTreeCost} AS cost_usd
1960
+ FROM ${pullRequests}
1961
+ CROSS JOIN LATERAL (
1962
+ SELECT ${pullRequestConversationIds} AS ids
1963
+ ) AS conversation_ids
1964
+ WHERE ${pullRequests.openedAt} >= ${oldestStart}
1965
+ ), issue_entities AS (
1966
+ SELECT
1967
+ ${issues.openedAt} AS opened_at,
1968
+ conversation_ids.ids AS ids,
1969
+ ${conversationTreeCost} AS cost_usd
1970
+ FROM ${issues}
1971
+ CROSS JOIN LATERAL (
1972
+ SELECT ${issueConversationIds} AS ids
1973
+ ) AS conversation_ids
1974
+ WHERE ${issues.openedAt} >= ${oldestStart}
1975
+ ), pull_request_window AS (
1976
+ SELECT
1977
+ windows.days AS days,
1978
+ coalesce((
1979
+ SELECT ${conversationTreeCost}
1980
+ FROM (
1981
+ SELECT ARRAY(
1982
+ SELECT DISTINCT unnest(pull_request_entities.ids)
1983
+ FROM pull_request_entities
1984
+ WHERE pull_request_entities.opened_at >= windows.start_at
1985
+ ) AS ids
1986
+ ) AS conversation_ids
1987
+ ), 0)::double precision AS pull_request_cost_usd,
1988
+ (
1989
+ percentile_cont(0.5) WITHIN GROUP (
1990
+ ORDER BY pull_request_entities.cost_usd
1991
+ ) FILTER (
1992
+ WHERE pull_request_entities.opened_at >= windows.start_at
1993
+ AND pull_request_entities.cost_usd > 0
1994
+ )
1995
+ )::double precision AS median_pull_request_cost_usd
1996
+ FROM windows
1997
+ LEFT JOIN pull_request_entities ON true
1998
+ GROUP BY windows.days, windows.start_at
1999
+ ), issue_window AS (
2000
+ SELECT
2001
+ windows.days AS days,
2002
+ coalesce((
2003
+ SELECT ${conversationTreeCost}
2004
+ FROM (
2005
+ SELECT ARRAY(
2006
+ SELECT DISTINCT unnest(issue_entities.ids)
2007
+ FROM issue_entities
2008
+ WHERE issue_entities.opened_at >= windows.start_at
2009
+ ) AS ids
2010
+ ) AS conversation_ids
2011
+ ), 0)::double precision AS issue_cost_usd,
2012
+ (
2013
+ percentile_cont(0.5) WITHIN GROUP (
2014
+ ORDER BY issue_entities.cost_usd
2015
+ ) FILTER (
2016
+ WHERE issue_entities.opened_at >= windows.start_at
2017
+ AND issue_entities.cost_usd > 0
2018
+ )
2019
+ )::double precision AS median_issue_cost_usd
2020
+ FROM windows
2021
+ LEFT JOIN issue_entities ON true
2022
+ GROUP BY windows.days, windows.start_at
2023
+ )
2024
+ SELECT
2025
+ pull_request_window.days AS "days",
2026
+ pull_request_window.pull_request_cost_usd AS "pullRequestCostUsd",
2027
+ pull_request_window.median_pull_request_cost_usd
2028
+ AS "medianPullRequestCostUsd",
2029
+ issue_window.issue_cost_usd AS "issueCostUsd",
2030
+ issue_window.median_issue_cost_usd AS "medianIssueCostUsd"
2031
+ FROM pull_request_window
2032
+ INNER JOIN issue_window ON issue_window.days = pull_request_window.days
2033
+ ORDER BY pull_request_window.days
2034
+ `);
2035
+ return z10.array(costWindowSchema).parse(queryRows(result));
2036
+ }
2037
+ async function aggregateGitHubRepositoryCosts(args) {
2038
+ if (!await hasConversationUsageTable(args.db)) {
2039
+ return [];
2040
+ }
2041
+ const start = new Date(args.nowMs - 30 * DAY_MS);
2042
+ const pullRequests = juniorGitHubPullRequests;
2043
+ const issues = juniorGitHubIssues;
2044
+ const conversationTreeCost = conversationTreeCostExpr();
2045
+ const pullRequestConversationIds = pullRequestConversationIdsExpr();
2046
+ const issueConversationIds = issueConversationIdsExpr();
2047
+ const result = await args.db.execute(sql4`
2048
+ WITH pull_request_entities AS (
2049
+ SELECT
2050
+ ${pullRequests.repositoryFullName} AS repository,
2051
+ conversation_ids.ids AS ids
2052
+ FROM ${pullRequests}
2053
+ CROSS JOIN LATERAL (
2054
+ SELECT ${pullRequestConversationIds} AS ids
2055
+ ) AS conversation_ids
2056
+ WHERE ${pullRequests.openedAt} >= ${start}
2057
+ ), issue_entities AS (
2058
+ SELECT
2059
+ ${issues.repositoryFullName} AS repository,
2060
+ conversation_ids.ids AS ids
2061
+ FROM ${issues}
2062
+ CROSS JOIN LATERAL (
2063
+ SELECT ${issueConversationIds} AS ids
2064
+ ) AS conversation_ids
2065
+ WHERE ${issues.openedAt} >= ${start}
2066
+ ), repositories AS (
2067
+ SELECT repository FROM pull_request_entities
2068
+ UNION
2069
+ SELECT repository FROM issue_entities
2070
+ ), pull_request_totals AS (
2071
+ SELECT
2072
+ repositories.repository AS repository,
2073
+ coalesce((
2074
+ SELECT ${conversationTreeCost}
2075
+ FROM (
2076
+ SELECT ARRAY(
2077
+ SELECT DISTINCT unnest(pull_request_entities.ids)
2078
+ FROM pull_request_entities
2079
+ WHERE pull_request_entities.repository = repositories.repository
2080
+ ) AS ids
2081
+ ) AS conversation_ids
2082
+ ), 0)::double precision AS pull_request_cost_usd
2083
+ FROM repositories
2084
+ ), issue_totals AS (
2085
+ SELECT
2086
+ repositories.repository AS repository,
2087
+ coalesce((
2088
+ SELECT ${conversationTreeCost}
2089
+ FROM (
2090
+ SELECT ARRAY(
2091
+ SELECT DISTINCT unnest(issue_entities.ids)
2092
+ FROM issue_entities
2093
+ WHERE issue_entities.repository = repositories.repository
2094
+ ) AS ids
2095
+ ) AS conversation_ids
2096
+ ), 0)::double precision AS issue_cost_usd
2097
+ FROM repositories
2098
+ )
2099
+ SELECT
2100
+ repositories.repository AS "repository",
2101
+ coalesce(pull_request_totals.pull_request_cost_usd, 0)::double precision
2102
+ AS "pullRequestCostUsd",
2103
+ coalesce(issue_totals.issue_cost_usd, 0)::double precision
2104
+ AS "issueCostUsd"
2105
+ FROM repositories
2106
+ LEFT JOIN pull_request_totals
2107
+ ON pull_request_totals.repository = repositories.repository
2108
+ LEFT JOIN issue_totals
2109
+ ON issue_totals.repository = repositories.repository
2110
+ ORDER BY "pullRequestCostUsd" DESC, "issueCostUsd" DESC, "repository" ASC
2111
+ `);
2112
+ return z10.array(repositoryCostSchema).parse(queryRows(result));
2113
+ }
2114
+ function formatCostUsd(value) {
2115
+ if (value === void 0) return "\u2014";
2116
+ return new Intl.NumberFormat("en-US", {
2117
+ style: "currency",
2118
+ currency: "USD",
2119
+ minimumFractionDigits: 2,
2120
+ maximumFractionDigits: 2
2121
+ }).format(value);
2122
+ }
2123
+
2124
+ // src/outcomes/report.ts
2125
+ var DAY_MS2 = 24 * 60 * 60 * 1e3;
2126
+ var WINDOWS = [7, 30, 90];
2127
+ var pullRequestStatsSchema = z11.object({
2128
+ closed: z11.number().int().nonnegative(),
2129
+ created: z11.number().int().nonnegative(),
2130
+ days: z11.number().int().positive(),
2131
+ medianMergeTimeMs: z11.number().nonnegative().nullable(),
2132
+ merged: z11.number().int().nonnegative()
1653
2133
  }).strict().transform((row) => {
1654
2134
  const terminal = row.merged + row.closed;
1655
- const classified = row.juniorOnly + row.mixed;
1656
2135
  return {
1657
2136
  ...row,
1658
2137
  medianMergeTimeMs: row.medianMergeTimeMs ?? void 0,
1659
- mergeRate: terminal > 0 ? row.merged / terminal : void 0,
1660
- juniorOnlyRate: classified > 0 ? row.juniorOnly / classified : void 0
2138
+ mergeRate: terminal > 0 ? row.merged / terminal : void 0
1661
2139
  };
1662
2140
  });
1663
- var pullRequestRepositoryStatsSchema = z10.object({
1664
- closed: z10.number().int().nonnegative(),
1665
- compositionUnknown: z10.number().int().nonnegative(),
1666
- created: z10.number().int().nonnegative(),
1667
- juniorOnly: z10.number().int().nonnegative(),
1668
- merged: z10.number().int().nonnegative(),
1669
- mixed: z10.number().int().nonnegative(),
1670
- repository: z10.string().min(1)
2141
+ var pullRequestRepositoryStatsSchema = z11.object({
2142
+ closed: z11.number().int().nonnegative(),
2143
+ created: z11.number().int().nonnegative(),
2144
+ juniorOnly: z11.number().int().nonnegative(),
2145
+ merged: z11.number().int().nonnegative(),
2146
+ repository: z11.string().min(1)
1671
2147
  }).strict().transform((row) => {
1672
2148
  const terminal = row.merged + row.closed;
1673
2149
  return {
@@ -1675,41 +2151,35 @@ var pullRequestRepositoryStatsSchema = z10.object({
1675
2151
  mergeRate: terminal > 0 ? row.merged / terminal : void 0
1676
2152
  };
1677
2153
  });
1678
- var issueStatsSchema = z10.object({
1679
- closedCompleted: z10.number().int().nonnegative(),
1680
- closedDuplicate: z10.number().int().nonnegative(),
1681
- closedNotPlanned: z10.number().int().nonnegative(),
1682
- closedUnknown: z10.number().int().nonnegative(),
1683
- created: z10.number().int().nonnegative(),
1684
- days: z10.number().int().positive(),
1685
- medianCloseTimeMs: z10.number().nonnegative().nullable()
2154
+ var issueStatsSchema = z11.object({
2155
+ closedCompleted: z11.number().int().nonnegative(),
2156
+ closedDuplicate: z11.number().int().nonnegative(),
2157
+ closedNotPlanned: z11.number().int().nonnegative(),
2158
+ closedUnknown: z11.number().int().nonnegative(),
2159
+ created: z11.number().int().nonnegative(),
2160
+ days: z11.number().int().positive(),
2161
+ medianCloseTimeMs: z11.number().nonnegative().nullable()
1686
2162
  }).strict().transform((row) => ({
1687
2163
  ...row,
1688
2164
  medianCloseTimeMs: row.medianCloseTimeMs ?? void 0
1689
2165
  }));
1690
- var pullRequestDaySchema = z10.object({
1691
- closed: z10.number().int().nonnegative(),
1692
- created: z10.number().int().nonnegative(),
1693
- date: z10.string().date(),
1694
- merged: z10.number().int().nonnegative()
2166
+ var pullRequestDaySchema = z11.object({
2167
+ created: z11.number().int().nonnegative(),
2168
+ date: z11.string().date()
1695
2169
  }).strict();
1696
- var issueDaySchema = z10.object({
1697
- closedCompleted: z10.number().int().nonnegative(),
1698
- closedDuplicate: z10.number().int().nonnegative(),
1699
- closedNotPlanned: z10.number().int().nonnegative(),
1700
- closedUnknown: z10.number().int().nonnegative(),
1701
- created: z10.number().int().nonnegative(),
1702
- date: z10.string().date()
2170
+ var issueDaySchema = z11.object({
2171
+ created: z11.number().int().nonnegative(),
2172
+ date: z11.string().date()
1703
2173
  }).strict();
1704
- var issueRepositoryStatsSchema = z10.object({
1705
- closedCompleted: z10.number().int().nonnegative(),
1706
- closedDuplicate: z10.number().int().nonnegative(),
1707
- closedNotPlanned: z10.number().int().nonnegative(),
1708
- closedUnknown: z10.number().int().nonnegative(),
1709
- created: z10.number().int().nonnegative(),
1710
- repository: z10.string().min(1)
2174
+ var issueRepositoryStatsSchema = z11.object({
2175
+ closedCompleted: z11.number().int().nonnegative(),
2176
+ closedDuplicate: z11.number().int().nonnegative(),
2177
+ closedNotPlanned: z11.number().int().nonnegative(),
2178
+ closedUnknown: z11.number().int().nonnegative(),
2179
+ created: z11.number().int().nonnegative(),
2180
+ repository: z11.string().min(1)
1711
2181
  }).strict();
1712
- function queryRows(result) {
2182
+ function queryRows2(result) {
1713
2183
  if (typeof result !== "object" || result === null || !("rows" in result) || !Array.isArray(result.rows)) {
1714
2184
  throw new TypeError("GitHub outcome query did not return rows");
1715
2185
  }
@@ -1717,11 +2187,11 @@ function queryRows(result) {
1717
2187
  }
1718
2188
  async function aggregatePullRequestWindows(args) {
1719
2189
  const starts = WINDOWS.map(
1720
- (days) => [days, new Date(args.nowMs - days * DAY_MS)]
2190
+ (days) => [days, new Date(args.nowMs - days * DAY_MS2)]
1721
2191
  );
1722
2192
  const oldestStart = starts.at(-1)[1];
1723
2193
  const table = juniorGitHubPullRequests;
1724
- const result = await args.db.execute(sql3`
2194
+ const result = await args.db.execute(sql5`
1725
2195
  WITH windows(days, start_at) AS (
1726
2196
  VALUES
1727
2197
  (${starts[0][0]}::integer, ${starts[0][1]}::timestamptz),
@@ -1731,7 +2201,6 @@ async function aggregatePullRequestWindows(args) {
1731
2201
  SELECT
1732
2202
  ${table.pullRequestId},
1733
2203
  ${table.state},
1734
- ${table.commitComposition},
1735
2204
  ${table.openedAt},
1736
2205
  ${table.mergedAt},
1737
2206
  ${table.closedAt}
@@ -1755,24 +2224,6 @@ async function aggregatePullRequestWindows(args) {
1755
2224
  WHERE recent_pull_requests.state = 'closed_unmerged'
1756
2225
  AND recent_pull_requests.closed_at >= windows.start_at
1757
2226
  )::integer AS "closed",
1758
- count(recent_pull_requests.pull_request_id)
1759
- FILTER (
1760
- WHERE recent_pull_requests.state = 'merged'
1761
- AND recent_pull_requests.merged_at >= windows.start_at
1762
- AND recent_pull_requests.commit_composition = 'junior_only'
1763
- )::integer AS "juniorOnly",
1764
- count(recent_pull_requests.pull_request_id)
1765
- FILTER (
1766
- WHERE recent_pull_requests.state = 'merged'
1767
- AND recent_pull_requests.merged_at >= windows.start_at
1768
- AND recent_pull_requests.commit_composition = 'mixed'
1769
- )::integer AS "mixed",
1770
- count(recent_pull_requests.pull_request_id)
1771
- FILTER (
1772
- WHERE recent_pull_requests.state = 'merged'
1773
- AND recent_pull_requests.merged_at >= windows.start_at
1774
- AND recent_pull_requests.commit_composition IS NULL
1775
- )::integer AS "compositionUnknown",
1776
2227
  (
1777
2228
  percentile_cont(0.5) WITHIN GROUP (
1778
2229
  ORDER BY extract(
@@ -1790,61 +2241,40 @@ async function aggregatePullRequestWindows(args) {
1790
2241
  GROUP BY windows.days
1791
2242
  ORDER BY windows.days
1792
2243
  `);
1793
- return z10.array(pullRequestStatsSchema).parse(queryRows(result));
2244
+ return z11.array(pullRequestStatsSchema).parse(queryRows2(result));
1794
2245
  }
1795
2246
  async function aggregatePullRequestDays(args) {
1796
2247
  const end = new Date(args.nowMs);
1797
- const start = startOfUtcDay(args.nowMs - (WINDOWS.at(-1) - 1) * DAY_MS);
2248
+ const start = startOfUtcDay(args.nowMs - (WINDOWS.at(-1) - 1) * DAY_MS2);
1798
2249
  const table = juniorGitHubPullRequests;
1799
- const result = await args.db.execute(sql3`
2250
+ const result = await args.db.execute(sql5`
1800
2251
  WITH days AS (
1801
2252
  SELECT generate_series(
1802
2253
  date_trunc('day', ${start}::timestamptz AT TIME ZONE 'UTC'),
1803
2254
  date_trunc('day', ${end}::timestamptz AT TIME ZONE 'UTC'),
1804
2255
  interval '1 day'
1805
2256
  ) AS day
1806
- ), events AS (
2257
+ ), daily AS (
1807
2258
  SELECT
1808
2259
  date_trunc('day', ${table.openedAt} AT TIME ZONE 'UTC') AS day,
1809
- 'created' AS kind
2260
+ count(*)::integer AS created
1810
2261
  FROM ${table}
1811
2262
  WHERE ${table.openedAt} >= ${start}
1812
- UNION ALL
1813
- SELECT
1814
- date_trunc('day', ${table.mergedAt} AT TIME ZONE 'UTC') AS day,
1815
- 'merged' AS kind
1816
- FROM ${table}
1817
- WHERE ${table.state} = 'merged' AND ${table.mergedAt} >= ${start}
1818
- UNION ALL
1819
- SELECT
1820
- date_trunc('day', ${table.closedAt} AT TIME ZONE 'UTC') AS day,
1821
- 'closed' AS kind
1822
- FROM ${table}
1823
- WHERE ${table.state} = 'closed_unmerged' AND ${table.closedAt} >= ${start}
1824
- ), daily AS (
1825
- SELECT
1826
- events.day,
1827
- count(*) FILTER (WHERE events.kind = 'created')::integer AS created,
1828
- count(*) FILTER (WHERE events.kind = 'merged')::integer AS merged,
1829
- count(*) FILTER (WHERE events.kind = 'closed')::integer AS closed
1830
- FROM events
1831
- GROUP BY events.day
2263
+ GROUP BY date_trunc('day', ${table.openedAt} AT TIME ZONE 'UTC')
1832
2264
  )
1833
2265
  SELECT
1834
2266
  to_char(days.day, 'YYYY-MM-DD') AS "date",
1835
- coalesce(daily.created, 0)::integer AS "created",
1836
- coalesce(daily.merged, 0)::integer AS "merged",
1837
- coalesce(daily.closed, 0)::integer AS "closed"
2267
+ coalesce(daily.created, 0)::integer AS "created"
1838
2268
  FROM days
1839
2269
  LEFT JOIN daily ON daily.day = days.day
1840
2270
  ORDER BY days.day
1841
2271
  `);
1842
- return z10.array(pullRequestDaySchema).parse(queryRows(result));
2272
+ return z11.array(pullRequestDaySchema).parse(queryRows2(result));
1843
2273
  }
1844
2274
  async function aggregatePullRequestRepositories(args) {
1845
- const start = new Date(args.nowMs - 30 * DAY_MS);
2275
+ const start = new Date(args.nowMs - 30 * DAY_MS2);
1846
2276
  const table = juniorGitHubPullRequests;
1847
- const result = await args.db.execute(sql3`
2277
+ const result = await args.db.execute(sql5`
1848
2278
  SELECT
1849
2279
  ${table.repositoryFullName} AS "repository",
1850
2280
  count(*) FILTER (WHERE ${table.openedAt} >= ${start})::integer
@@ -1860,17 +2290,7 @@ async function aggregatePullRequestRepositories(args) {
1860
2290
  WHERE ${table.state} = 'merged'
1861
2291
  AND ${table.mergedAt} >= ${start}
1862
2292
  AND ${table.commitComposition} = 'junior_only'
1863
- )::integer AS "juniorOnly",
1864
- count(*) FILTER (
1865
- WHERE ${table.state} = 'merged'
1866
- AND ${table.mergedAt} >= ${start}
1867
- AND ${table.commitComposition} = 'mixed'
1868
- )::integer AS "mixed",
1869
- count(*) FILTER (
1870
- WHERE ${table.state} = 'merged'
1871
- AND ${table.mergedAt} >= ${start}
1872
- AND ${table.commitComposition} IS NULL
1873
- )::integer AS "compositionUnknown"
2293
+ )::integer AS "juniorOnly"
1874
2294
  FROM ${table}
1875
2295
  WHERE ${table.openedAt} >= ${start}
1876
2296
  OR ${table.mergedAt} >= ${start}
@@ -1879,15 +2299,15 @@ async function aggregatePullRequestRepositories(args) {
1879
2299
  ORDER BY "merged" DESC, "created" DESC, "repository" ASC
1880
2300
  LIMIT 25
1881
2301
  `);
1882
- return z10.array(pullRequestRepositoryStatsSchema).parse(queryRows(result));
2302
+ return z11.array(pullRequestRepositoryStatsSchema).parse(queryRows2(result));
1883
2303
  }
1884
2304
  async function aggregateIssueWindows(args) {
1885
2305
  const starts = WINDOWS.map(
1886
- (days) => [days, new Date(args.nowMs - days * DAY_MS)]
2306
+ (days) => [days, new Date(args.nowMs - days * DAY_MS2)]
1887
2307
  );
1888
2308
  const oldestStart = starts.at(-1)[1];
1889
2309
  const table = juniorGitHubIssues;
1890
- const result = await args.db.execute(sql3`
2310
+ const result = await args.db.execute(sql5`
1891
2311
  WITH windows(days, start_at) AS (
1892
2312
  VALUES
1893
2313
  (${starts[0][0]}::integer, ${starts[0][1]}::timestamptz),
@@ -1948,63 +2368,40 @@ async function aggregateIssueWindows(args) {
1948
2368
  GROUP BY windows.days
1949
2369
  ORDER BY windows.days
1950
2370
  `);
1951
- return z10.array(issueStatsSchema).parse(queryRows(result));
2371
+ return z11.array(issueStatsSchema).parse(queryRows2(result));
1952
2372
  }
1953
2373
  async function aggregateIssueDays(args) {
1954
2374
  const end = new Date(args.nowMs);
1955
- const start = startOfUtcDay(args.nowMs - (WINDOWS.at(-1) - 1) * DAY_MS);
2375
+ const start = startOfUtcDay(args.nowMs - (WINDOWS.at(-1) - 1) * DAY_MS2);
1956
2376
  const table = juniorGitHubIssues;
1957
- const result = await args.db.execute(sql3`
2377
+ const result = await args.db.execute(sql5`
1958
2378
  WITH days AS (
1959
2379
  SELECT generate_series(
1960
2380
  date_trunc('day', ${start}::timestamptz AT TIME ZONE 'UTC'),
1961
2381
  date_trunc('day', ${end}::timestamptz AT TIME ZONE 'UTC'),
1962
2382
  interval '1 day'
1963
2383
  ) AS day
1964
- ), events AS (
2384
+ ), daily AS (
1965
2385
  SELECT
1966
2386
  date_trunc('day', ${table.openedAt} AT TIME ZONE 'UTC') AS day,
1967
- 'created' AS kind
2387
+ count(*)::integer AS created
1968
2388
  FROM ${table}
1969
2389
  WHERE ${table.openedAt} >= ${start}
1970
- UNION ALL
1971
- SELECT
1972
- date_trunc('day', ${table.closedAt} AT TIME ZONE 'UTC') AS day,
1973
- coalesce(${table.stateReason}, 'unknown') AS kind
1974
- FROM ${table}
1975
- WHERE ${table.state} = 'closed' AND ${table.closedAt} >= ${start}
1976
- ), daily AS (
1977
- SELECT
1978
- events.day,
1979
- count(*) FILTER (WHERE events.kind = 'created')::integer AS created,
1980
- count(*) FILTER (WHERE events.kind = 'completed')::integer
1981
- AS closed_completed,
1982
- count(*) FILTER (WHERE events.kind = 'duplicate')::integer
1983
- AS closed_duplicate,
1984
- count(*) FILTER (WHERE events.kind = 'not_planned')::integer
1985
- AS closed_not_planned,
1986
- count(*) FILTER (WHERE events.kind = 'unknown')::integer
1987
- AS closed_unknown
1988
- FROM events
1989
- GROUP BY events.day
2390
+ GROUP BY date_trunc('day', ${table.openedAt} AT TIME ZONE 'UTC')
1990
2391
  )
1991
2392
  SELECT
1992
2393
  to_char(days.day, 'YYYY-MM-DD') AS "date",
1993
- coalesce(daily.created, 0)::integer AS "created",
1994
- coalesce(daily.closed_completed, 0)::integer AS "closedCompleted",
1995
- coalesce(daily.closed_duplicate, 0)::integer AS "closedDuplicate",
1996
- coalesce(daily.closed_not_planned, 0)::integer AS "closedNotPlanned",
1997
- coalesce(daily.closed_unknown, 0)::integer AS "closedUnknown"
2394
+ coalesce(daily.created, 0)::integer AS "created"
1998
2395
  FROM days
1999
2396
  LEFT JOIN daily ON daily.day = days.day
2000
2397
  ORDER BY days.day
2001
2398
  `);
2002
- return z10.array(issueDaySchema).parse(queryRows(result));
2399
+ return z11.array(issueDaySchema).parse(queryRows2(result));
2003
2400
  }
2004
2401
  async function aggregateIssueRepositories(args) {
2005
- const start = new Date(args.nowMs - 30 * DAY_MS);
2402
+ const start = new Date(args.nowMs - 30 * DAY_MS2);
2006
2403
  const table = juniorGitHubIssues;
2007
- const result = await args.db.execute(sql3`
2404
+ const result = await args.db.execute(sql5`
2008
2405
  SELECT
2009
2406
  ${table.repositoryFullName} AS "repository",
2010
2407
  count(*) FILTER (WHERE ${table.openedAt} >= ${start})::integer
@@ -2036,7 +2433,7 @@ async function aggregateIssueRepositories(args) {
2036
2433
  ORDER BY "created" DESC, "closedCompleted" DESC, "repository" ASC
2037
2434
  LIMIT 25
2038
2435
  `);
2039
- return z10.array(issueRepositoryStatsSchema).parse(queryRows(result));
2436
+ return z11.array(issueRepositoryStatsSchema).parse(queryRows2(result));
2040
2437
  }
2041
2438
  function formatPercent(value) {
2042
2439
  return value === void 0 ? "\u2014" : `${Math.round(value * 100)}%`;
@@ -2048,9 +2445,6 @@ function formatDuration(value) {
2048
2445
  if (hours < 24) return `${Math.round(hours * 10) / 10}h`;
2049
2446
  return `${Math.round(hours / 24 * 10) / 10}d`;
2050
2447
  }
2051
- function formatCommitComposition(stats) {
2052
- return `${stats.juniorOnly} Junior-only \xB7 ${stats.mixed} mixed \xB7 ${stats.compositionUnknown} unknown`;
2053
- }
2054
2448
  function startOfUtcDay(timestampMs) {
2055
2449
  const date = new Date(timestampMs);
2056
2450
  date.setUTCHours(0, 0, 0, 0);
@@ -2063,83 +2457,83 @@ async function buildGitHubOutcomeReport(args) {
2063
2457
  repositories,
2064
2458
  issueWindows,
2065
2459
  issueDays,
2066
- issueRepositories
2460
+ issueRepositories,
2461
+ costWindows,
2462
+ repositoryCosts
2067
2463
  ] = await Promise.all([
2068
2464
  aggregatePullRequestWindows(args),
2069
2465
  aggregatePullRequestDays(args),
2070
2466
  aggregatePullRequestRepositories(args),
2071
2467
  aggregateIssueWindows(args),
2072
2468
  aggregateIssueDays(args),
2073
- aggregateIssueRepositories(args)
2469
+ aggregateIssueRepositories(args),
2470
+ aggregateGitHubCostWindows({ ...args, windows: WINDOWS }),
2471
+ aggregateGitHubRepositoryCosts(args)
2074
2472
  ]);
2075
2473
  const thirtyDays = windows.find((window) => window.days === 30);
2076
2474
  const issueThirtyDays = issueWindows.find((window) => window.days === 30);
2475
+ const costThirtyDays = costWindows.find((window) => window.days === 30);
2476
+ const repositoryCostByName = new Map(
2477
+ repositoryCosts.map((row) => [row.repository, row])
2478
+ );
2077
2479
  return {
2078
2480
  generatedAt: new Date(args.nowMs).toISOString(),
2079
- title: "GitHub work delivered",
2481
+ title: "GitHub activity",
2080
2482
  metrics: [
2081
2483
  {
2082
- label: "Junior-only merge rate \xB7 30d",
2083
- value: formatPercent(thirtyDays.juniorOnlyRate)
2084
- },
2085
- {
2086
- label: "PR merge rate \xB7 30d",
2484
+ label: "PR closure merge rate \xB7 30d",
2087
2485
  value: formatPercent(thirtyDays.mergeRate)
2088
2486
  },
2089
2487
  {
2090
- label: "median PR merge time \xB7 30d",
2488
+ label: "Median PR merge time \xB7 merged in 30d",
2091
2489
  value: formatDuration(thirtyDays.medianMergeTimeMs)
2092
2490
  },
2093
2491
  {
2094
- label: "median issue close time \xB7 30d",
2492
+ label: "Median issue close time \xB7 closed in 30d",
2095
2493
  value: formatDuration(issueThirtyDays.medianCloseTimeMs)
2494
+ },
2495
+ {
2496
+ label: "PR cost \xB7 opened in 30d",
2497
+ value: formatCostUsd(costThirtyDays.pullRequestCostUsd)
2498
+ },
2499
+ {
2500
+ label: "Median PR cost \xB7 opened in 30d",
2501
+ value: formatCostUsd(costThirtyDays.medianPullRequestCostUsd)
2502
+ },
2503
+ {
2504
+ label: "Issue cost \xB7 opened in 30d",
2505
+ value: formatCostUsd(costThirtyDays.issueCostUsd)
2506
+ },
2507
+ {
2508
+ label: "Median issue cost \xB7 opened in 30d",
2509
+ value: formatCostUsd(costThirtyDays.medianIssueCostUsd)
2096
2510
  }
2097
2511
  ],
2098
2512
  widgets: [
2099
2513
  {
2100
- id: "pull-request-outcomes",
2514
+ id: "pull-requests-created",
2101
2515
  type: "bar_chart",
2102
- title: "Pull request outcomes",
2103
- description: "Daily outcomes",
2516
+ title: "Pull requests created",
2517
+ description: "Junior-owned pull requests opened per day",
2104
2518
  timeRangeDays: [...WINDOWS],
2105
- series: [
2106
- { key: "created", label: "Created" },
2107
- { key: "merged", label: "Merged", tone: "good" },
2108
- { key: "closed", label: "Closed unmerged", tone: "danger" }
2109
- ],
2519
+ series: [{ key: "created", label: "Created" }],
2110
2520
  categories: pullRequestDays.map((stats) => ({
2111
2521
  id: stats.date,
2112
2522
  label: stats.date,
2113
- values: {
2114
- created: stats.created,
2115
- merged: stats.merged,
2116
- closed: stats.closed
2117
- }
2523
+ values: { created: stats.created }
2118
2524
  }))
2119
2525
  },
2120
2526
  {
2121
- id: "issue-outcomes",
2527
+ id: "issues-created",
2122
2528
  type: "bar_chart",
2123
- title: "Issue outcomes",
2124
- description: "Daily outcomes",
2529
+ title: "Issues created",
2530
+ description: "Junior-owned issues opened per day",
2125
2531
  timeRangeDays: [...WINDOWS],
2126
- series: [
2127
- { key: "created", label: "Created" },
2128
- { key: "completed", label: "Completed", tone: "good" },
2129
- { key: "duplicate", label: "Duplicate", tone: "warning" },
2130
- { key: "notPlanned", label: "Not planned", tone: "danger" },
2131
- { key: "unknown", label: "Unknown" }
2132
- ],
2532
+ series: [{ key: "created", label: "Created" }],
2133
2533
  categories: issueDays.map((stats) => ({
2134
2534
  id: stats.date,
2135
2535
  label: stats.date,
2136
- values: {
2137
- created: stats.created,
2138
- completed: stats.closedCompleted,
2139
- duplicate: stats.closedDuplicate,
2140
- notPlanned: stats.closedNotPlanned,
2141
- unknown: stats.closedUnknown
2142
- }
2536
+ values: { created: stats.created }
2143
2537
  }))
2144
2538
  }
2145
2539
  ],
@@ -2150,10 +2544,11 @@ async function buildGitHubOutcomeReport(args) {
2150
2544
  fields: [
2151
2545
  { key: "repository", label: "Repository" },
2152
2546
  { key: "created", label: "Created" },
2547
+ { key: "juniorOnly", label: "Junior-only merges" },
2153
2548
  { key: "merged", label: "Merged" },
2154
2549
  { key: "closed", label: "Closed unmerged" },
2155
- { key: "commitComposition", label: "Merged commit composition" },
2156
- { key: "mergeRate", label: "Merge rate" }
2550
+ { key: "mergeRate", label: "Closure merge rate" },
2551
+ { key: "cost", label: "Cost" }
2157
2552
  ],
2158
2553
  records: repositories.map(({ repository, ...stats }) => ({
2159
2554
  id: repository,
@@ -2162,8 +2557,11 @@ async function buildGitHubOutcomeReport(args) {
2162
2557
  created: String(stats.created),
2163
2558
  merged: String(stats.merged),
2164
2559
  closed: String(stats.closed),
2165
- commitComposition: formatCommitComposition(stats),
2166
- mergeRate: formatPercent(stats.mergeRate)
2560
+ juniorOnly: String(stats.juniorOnly),
2561
+ mergeRate: formatPercent(stats.mergeRate),
2562
+ cost: formatCostUsd(
2563
+ repositoryCostByName.get(repository)?.pullRequestCostUsd ?? 0
2564
+ )
2167
2565
  }
2168
2566
  }))
2169
2567
  },
@@ -2176,7 +2574,8 @@ async function buildGitHubOutcomeReport(args) {
2176
2574
  { key: "completed", label: "Completed" },
2177
2575
  { key: "duplicate", label: "Duplicate" },
2178
2576
  { key: "notPlanned", label: "Not planned" },
2179
- { key: "unknown", label: "Unknown reason" }
2577
+ { key: "unknown", label: "Unknown reason" },
2578
+ { key: "cost", label: "Cost" }
2180
2579
  ],
2181
2580
  records: issueRepositories.map(({ repository, ...stats }) => ({
2182
2581
  id: repository,
@@ -2186,7 +2585,10 @@ async function buildGitHubOutcomeReport(args) {
2186
2585
  completed: String(stats.closedCompleted),
2187
2586
  duplicate: String(stats.closedDuplicate),
2188
2587
  notPlanned: String(stats.closedNotPlanned),
2189
- unknown: String(stats.closedUnknown)
2588
+ unknown: String(stats.closedUnknown),
2589
+ cost: formatCostUsd(
2590
+ repositoryCostByName.get(repository)?.issueCostUsd ?? 0
2591
+ )
2190
2592
  }
2191
2593
  }))
2192
2594
  }
@@ -2195,18 +2597,18 @@ async function buildGitHubOutcomeReport(args) {
2195
2597
  }
2196
2598
 
2197
2599
  // src/pull-request-outcomes/commit-composition.ts
2198
- import { z as z11 } from "zod";
2199
- var canonicalCommitSchema = z11.object({
2200
- authorEmail: z11.string().nullable(),
2201
- authorLogin: z11.string().nullable()
2600
+ import { z as z12 } from "zod";
2601
+ var canonicalCommitSchema = z12.object({
2602
+ authorEmail: z12.string().nullable(),
2603
+ authorLogin: z12.string().nullable()
2202
2604
  }).strict();
2203
- var providerCommitSchema = z11.object({
2204
- author: z11.object({ login: z11.string() }).passthrough().nullable(),
2205
- commit: z11.object({
2206
- author: z11.object({ email: z11.string() }).passthrough().nullable()
2605
+ var providerCommitSchema = z12.object({
2606
+ author: z12.object({ login: z12.string() }).passthrough().nullable(),
2607
+ commit: z12.object({
2608
+ author: z12.object({ email: z12.string() }).passthrough().nullable()
2207
2609
  }).passthrough()
2208
2610
  }).passthrough();
2209
- var commitPageSchema = z11.array(providerCommitSchema).transform(
2611
+ var commitPageSchema = z12.array(providerCommitSchema).transform(
2210
2612
  (commits) => commits.map(
2211
2613
  (commit) => canonicalCommitSchema.parse({
2212
2614
  authorEmail: commit.commit.author?.email ?? null,