@sentry/junior-github 0.112.0 → 0.114.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,16 +1806,330 @@ 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
- created: z10.number().int().nonnegative(),
1816
+ var costWindowSchema = z10.object({
1647
1817
  days: z10.number().int().positive(),
1648
- medianMergeTimeMs: z10.number().nonnegative().nullable(),
1649
- merged: 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()
1650
2133
  }).strict().transform((row) => {
1651
2134
  const terminal = row.merged + row.closed;
1652
2135
  return {
@@ -1655,12 +2138,12 @@ var pullRequestStatsSchema = z10.object({
1655
2138
  mergeRate: terminal > 0 ? row.merged / terminal : void 0
1656
2139
  };
1657
2140
  });
1658
- var pullRequestRepositoryStatsSchema = z10.object({
1659
- closed: z10.number().int().nonnegative(),
1660
- created: z10.number().int().nonnegative(),
1661
- juniorOnly: z10.number().int().nonnegative(),
1662
- merged: z10.number().int().nonnegative(),
1663
- 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)
1664
2147
  }).strict().transform((row) => {
1665
2148
  const terminal = row.merged + row.closed;
1666
2149
  return {
@@ -1668,35 +2151,35 @@ var pullRequestRepositoryStatsSchema = z10.object({
1668
2151
  mergeRate: terminal > 0 ? row.merged / terminal : void 0
1669
2152
  };
1670
2153
  });
1671
- var issueStatsSchema = z10.object({
1672
- closedCompleted: z10.number().int().nonnegative(),
1673
- closedDuplicate: z10.number().int().nonnegative(),
1674
- closedNotPlanned: z10.number().int().nonnegative(),
1675
- closedUnknown: z10.number().int().nonnegative(),
1676
- created: z10.number().int().nonnegative(),
1677
- days: z10.number().int().positive(),
1678
- 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()
1679
2162
  }).strict().transform((row) => ({
1680
2163
  ...row,
1681
2164
  medianCloseTimeMs: row.medianCloseTimeMs ?? void 0
1682
2165
  }));
1683
- var pullRequestDaySchema = z10.object({
1684
- created: z10.number().int().nonnegative(),
1685
- date: z10.string().date()
2166
+ var pullRequestDaySchema = z11.object({
2167
+ created: z11.number().int().nonnegative(),
2168
+ date: z11.string().date()
1686
2169
  }).strict();
1687
- var issueDaySchema = z10.object({
1688
- created: z10.number().int().nonnegative(),
1689
- date: z10.string().date()
2170
+ var issueDaySchema = z11.object({
2171
+ created: z11.number().int().nonnegative(),
2172
+ date: z11.string().date()
1690
2173
  }).strict();
1691
- var issueRepositoryStatsSchema = z10.object({
1692
- closedCompleted: z10.number().int().nonnegative(),
1693
- closedDuplicate: z10.number().int().nonnegative(),
1694
- closedNotPlanned: z10.number().int().nonnegative(),
1695
- closedUnknown: z10.number().int().nonnegative(),
1696
- created: z10.number().int().nonnegative(),
1697
- 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)
1698
2181
  }).strict();
1699
- function queryRows(result) {
2182
+ function queryRows2(result) {
1700
2183
  if (typeof result !== "object" || result === null || !("rows" in result) || !Array.isArray(result.rows)) {
1701
2184
  throw new TypeError("GitHub outcome query did not return rows");
1702
2185
  }
@@ -1704,11 +2187,11 @@ function queryRows(result) {
1704
2187
  }
1705
2188
  async function aggregatePullRequestWindows(args) {
1706
2189
  const starts = WINDOWS.map(
1707
- (days) => [days, new Date(args.nowMs - days * DAY_MS)]
2190
+ (days) => [days, new Date(args.nowMs - days * DAY_MS2)]
1708
2191
  );
1709
2192
  const oldestStart = starts.at(-1)[1];
1710
2193
  const table = juniorGitHubPullRequests;
1711
- const result = await args.db.execute(sql3`
2194
+ const result = await args.db.execute(sql5`
1712
2195
  WITH windows(days, start_at) AS (
1713
2196
  VALUES
1714
2197
  (${starts[0][0]}::integer, ${starts[0][1]}::timestamptz),
@@ -1758,13 +2241,13 @@ async function aggregatePullRequestWindows(args) {
1758
2241
  GROUP BY windows.days
1759
2242
  ORDER BY windows.days
1760
2243
  `);
1761
- return z10.array(pullRequestStatsSchema).parse(queryRows(result));
2244
+ return z11.array(pullRequestStatsSchema).parse(queryRows2(result));
1762
2245
  }
1763
2246
  async function aggregatePullRequestDays(args) {
1764
2247
  const end = new Date(args.nowMs);
1765
- const start = startOfUtcDay(args.nowMs - (WINDOWS.at(-1) - 1) * DAY_MS);
2248
+ const start = startOfUtcDay(args.nowMs - (WINDOWS.at(-1) - 1) * DAY_MS2);
1766
2249
  const table = juniorGitHubPullRequests;
1767
- const result = await args.db.execute(sql3`
2250
+ const result = await args.db.execute(sql5`
1768
2251
  WITH days AS (
1769
2252
  SELECT generate_series(
1770
2253
  date_trunc('day', ${start}::timestamptz AT TIME ZONE 'UTC'),
@@ -1786,12 +2269,12 @@ async function aggregatePullRequestDays(args) {
1786
2269
  LEFT JOIN daily ON daily.day = days.day
1787
2270
  ORDER BY days.day
1788
2271
  `);
1789
- return z10.array(pullRequestDaySchema).parse(queryRows(result));
2272
+ return z11.array(pullRequestDaySchema).parse(queryRows2(result));
1790
2273
  }
1791
2274
  async function aggregatePullRequestRepositories(args) {
1792
- const start = new Date(args.nowMs - 30 * DAY_MS);
2275
+ const start = new Date(args.nowMs - 30 * DAY_MS2);
1793
2276
  const table = juniorGitHubPullRequests;
1794
- const result = await args.db.execute(sql3`
2277
+ const result = await args.db.execute(sql5`
1795
2278
  SELECT
1796
2279
  ${table.repositoryFullName} AS "repository",
1797
2280
  count(*) FILTER (WHERE ${table.openedAt} >= ${start})::integer
@@ -1816,15 +2299,15 @@ async function aggregatePullRequestRepositories(args) {
1816
2299
  ORDER BY "merged" DESC, "created" DESC, "repository" ASC
1817
2300
  LIMIT 25
1818
2301
  `);
1819
- return z10.array(pullRequestRepositoryStatsSchema).parse(queryRows(result));
2302
+ return z11.array(pullRequestRepositoryStatsSchema).parse(queryRows2(result));
1820
2303
  }
1821
2304
  async function aggregateIssueWindows(args) {
1822
2305
  const starts = WINDOWS.map(
1823
- (days) => [days, new Date(args.nowMs - days * DAY_MS)]
2306
+ (days) => [days, new Date(args.nowMs - days * DAY_MS2)]
1824
2307
  );
1825
2308
  const oldestStart = starts.at(-1)[1];
1826
2309
  const table = juniorGitHubIssues;
1827
- const result = await args.db.execute(sql3`
2310
+ const result = await args.db.execute(sql5`
1828
2311
  WITH windows(days, start_at) AS (
1829
2312
  VALUES
1830
2313
  (${starts[0][0]}::integer, ${starts[0][1]}::timestamptz),
@@ -1885,13 +2368,13 @@ async function aggregateIssueWindows(args) {
1885
2368
  GROUP BY windows.days
1886
2369
  ORDER BY windows.days
1887
2370
  `);
1888
- return z10.array(issueStatsSchema).parse(queryRows(result));
2371
+ return z11.array(issueStatsSchema).parse(queryRows2(result));
1889
2372
  }
1890
2373
  async function aggregateIssueDays(args) {
1891
2374
  const end = new Date(args.nowMs);
1892
- const start = startOfUtcDay(args.nowMs - (WINDOWS.at(-1) - 1) * DAY_MS);
2375
+ const start = startOfUtcDay(args.nowMs - (WINDOWS.at(-1) - 1) * DAY_MS2);
1893
2376
  const table = juniorGitHubIssues;
1894
- const result = await args.db.execute(sql3`
2377
+ const result = await args.db.execute(sql5`
1895
2378
  WITH days AS (
1896
2379
  SELECT generate_series(
1897
2380
  date_trunc('day', ${start}::timestamptz AT TIME ZONE 'UTC'),
@@ -1913,12 +2396,12 @@ async function aggregateIssueDays(args) {
1913
2396
  LEFT JOIN daily ON daily.day = days.day
1914
2397
  ORDER BY days.day
1915
2398
  `);
1916
- return z10.array(issueDaySchema).parse(queryRows(result));
2399
+ return z11.array(issueDaySchema).parse(queryRows2(result));
1917
2400
  }
1918
2401
  async function aggregateIssueRepositories(args) {
1919
- const start = new Date(args.nowMs - 30 * DAY_MS);
2402
+ const start = new Date(args.nowMs - 30 * DAY_MS2);
1920
2403
  const table = juniorGitHubIssues;
1921
- const result = await args.db.execute(sql3`
2404
+ const result = await args.db.execute(sql5`
1922
2405
  SELECT
1923
2406
  ${table.repositoryFullName} AS "repository",
1924
2407
  count(*) FILTER (WHERE ${table.openedAt} >= ${start})::integer
@@ -1950,7 +2433,7 @@ async function aggregateIssueRepositories(args) {
1950
2433
  ORDER BY "created" DESC, "closedCompleted" DESC, "repository" ASC
1951
2434
  LIMIT 25
1952
2435
  `);
1953
- return z10.array(issueRepositoryStatsSchema).parse(queryRows(result));
2436
+ return z11.array(issueRepositoryStatsSchema).parse(queryRows2(result));
1954
2437
  }
1955
2438
  function formatPercent(value) {
1956
2439
  return value === void 0 ? "\u2014" : `${Math.round(value * 100)}%`;
@@ -1974,17 +2457,25 @@ async function buildGitHubOutcomeReport(args) {
1974
2457
  repositories,
1975
2458
  issueWindows,
1976
2459
  issueDays,
1977
- issueRepositories
2460
+ issueRepositories,
2461
+ costWindows,
2462
+ repositoryCosts
1978
2463
  ] = await Promise.all([
1979
2464
  aggregatePullRequestWindows(args),
1980
2465
  aggregatePullRequestDays(args),
1981
2466
  aggregatePullRequestRepositories(args),
1982
2467
  aggregateIssueWindows(args),
1983
2468
  aggregateIssueDays(args),
1984
- aggregateIssueRepositories(args)
2469
+ aggregateIssueRepositories(args),
2470
+ aggregateGitHubCostWindows({ ...args, windows: WINDOWS }),
2471
+ aggregateGitHubRepositoryCosts(args)
1985
2472
  ]);
1986
2473
  const thirtyDays = windows.find((window) => window.days === 30);
1987
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
+ );
1988
2479
  return {
1989
2480
  generatedAt: new Date(args.nowMs).toISOString(),
1990
2481
  title: "GitHub activity",
@@ -2000,6 +2491,22 @@ async function buildGitHubOutcomeReport(args) {
2000
2491
  {
2001
2492
  label: "Median issue close time \xB7 closed in 30d",
2002
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)
2003
2510
  }
2004
2511
  ],
2005
2512
  widgets: [
@@ -2040,7 +2547,8 @@ async function buildGitHubOutcomeReport(args) {
2040
2547
  { key: "juniorOnly", label: "Junior-only merges" },
2041
2548
  { key: "merged", label: "Merged" },
2042
2549
  { key: "closed", label: "Closed unmerged" },
2043
- { key: "mergeRate", label: "Closure merge rate" }
2550
+ { key: "mergeRate", label: "Closure merge rate" },
2551
+ { key: "cost", label: "Cost" }
2044
2552
  ],
2045
2553
  records: repositories.map(({ repository, ...stats }) => ({
2046
2554
  id: repository,
@@ -2050,7 +2558,10 @@ async function buildGitHubOutcomeReport(args) {
2050
2558
  merged: String(stats.merged),
2051
2559
  closed: String(stats.closed),
2052
2560
  juniorOnly: String(stats.juniorOnly),
2053
- mergeRate: formatPercent(stats.mergeRate)
2561
+ mergeRate: formatPercent(stats.mergeRate),
2562
+ cost: formatCostUsd(
2563
+ repositoryCostByName.get(repository)?.pullRequestCostUsd ?? 0
2564
+ )
2054
2565
  }
2055
2566
  }))
2056
2567
  },
@@ -2063,7 +2574,8 @@ async function buildGitHubOutcomeReport(args) {
2063
2574
  { key: "completed", label: "Completed" },
2064
2575
  { key: "duplicate", label: "Duplicate" },
2065
2576
  { key: "notPlanned", label: "Not planned" },
2066
- { key: "unknown", label: "Unknown reason" }
2577
+ { key: "unknown", label: "Unknown reason" },
2578
+ { key: "cost", label: "Cost" }
2067
2579
  ],
2068
2580
  records: issueRepositories.map(({ repository, ...stats }) => ({
2069
2581
  id: repository,
@@ -2073,7 +2585,10 @@ async function buildGitHubOutcomeReport(args) {
2073
2585
  completed: String(stats.closedCompleted),
2074
2586
  duplicate: String(stats.closedDuplicate),
2075
2587
  notPlanned: String(stats.closedNotPlanned),
2076
- unknown: String(stats.closedUnknown)
2588
+ unknown: String(stats.closedUnknown),
2589
+ cost: formatCostUsd(
2590
+ repositoryCostByName.get(repository)?.issueCostUsd ?? 0
2591
+ )
2077
2592
  }
2078
2593
  }))
2079
2594
  }
@@ -2082,18 +2597,18 @@ async function buildGitHubOutcomeReport(args) {
2082
2597
  }
2083
2598
 
2084
2599
  // src/pull-request-outcomes/commit-composition.ts
2085
- import { z as z11 } from "zod";
2086
- var canonicalCommitSchema = z11.object({
2087
- authorEmail: z11.string().nullable(),
2088
- 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()
2089
2604
  }).strict();
2090
- var providerCommitSchema = z11.object({
2091
- author: z11.object({ login: z11.string() }).passthrough().nullable(),
2092
- commit: z11.object({
2093
- 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()
2094
2609
  }).passthrough()
2095
2610
  }).passthrough();
2096
- var commitPageSchema = z11.array(providerCommitSchema).transform(
2611
+ var commitPageSchema = z12.array(providerCommitSchema).transform(
2097
2612
  (commits) => commits.map(
2098
2613
  (commit) => canonicalCommitSchema.parse({
2099
2614
  authorEmail: commit.commit.author?.email ?? null,