@preventive/triage 1.0.0-alpha.50 → 1.0.0-alpha.51

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.
Files changed (51) hide show
  1. package/common/managed/finding-detail.d.ts +2 -1
  2. package/common/managed/finding-detail.js +43 -9
  3. package/common/managed/npm-packages.d.ts +2 -0
  4. package/common/managed/npm-packages.js +18 -1
  5. package/common/managed/routes.js +19 -7
  6. package/common/managed/team-app.d.ts +1 -0
  7. package/common/managed/team-app.js +17 -1
  8. package/out/activity.js +4 -4
  9. package/out/{chunk-75YLHBCM.js → chunk-2K6UN3PC.js} +1 -1
  10. package/out/{chunk-2WDZPXVL.js → chunk-7JBSSRD7.js} +1 -1
  11. package/out/{chunk-4XMP7YVH.js → chunk-FMXWXDR4.js} +1 -1
  12. package/out/chunk-HT2SSA3V.js +1 -0
  13. package/out/chunk-IPAH4LKF.js +232 -0
  14. package/out/{chunk-FKBXNUW4.js → chunk-KOGNQDSJ.js} +1 -1
  15. package/out/{chunk-UK7RGJT6.js → chunk-VDRXHIHN.js} +2 -2
  16. package/out/{chunk-HTBYHSPG.js → chunk-WRAJVD35.js} +1 -1
  17. package/out/client-managed-import.js +1 -1
  18. package/out/client-managed.js +25 -25
  19. package/out/compare.js +14 -14
  20. package/out/dialogs.js +41 -41
  21. package/out/graph.js +1 -1
  22. package/out/index.html +6 -6
  23. package/out/scan.js +2 -2
  24. package/out/view.css +1 -1
  25. package/out/view.js +7 -7
  26. package/package.json +3 -1
  27. package/server-managed/agent-merges.ts +1 -1
  28. package/server-managed/app-agents.ts +4 -2
  29. package/server-managed/commit-lockfiles.ts +7 -4
  30. package/server-managed/comparison.ts +13 -2
  31. package/server-managed/db-neon.ts +13 -3
  32. package/server-managed/db.ts +2 -1
  33. package/server-managed/dependency-review-comments.ts +16 -18
  34. package/server-managed/dependency-reviews.ts +45 -27
  35. package/server-managed/github-app.ts +3 -42
  36. package/server-managed/github-requests.ts +97 -0
  37. package/server-managed/http.ts +249 -75
  38. package/server-managed/listed-activity.ts +39 -0
  39. package/server-managed/locked-packages.ts +28 -5
  40. package/server-managed/npm-kept.ts +14 -4
  41. package/server-managed/npm-patch.ts +325 -0
  42. package/server-managed/pull-dependencies.ts +9 -6
  43. package/server-managed/pull-dependency-reviews.ts +19 -13
  44. package/server-managed/repository-agents.ts +162 -69
  45. package/server-managed/repository-dependency-reviews.ts +6 -5
  46. package/server-managed/repository-pulls.ts +114 -58
  47. package/server-managed/repository-pulse.ts +1 -1
  48. package/server-managed/reviewed-dependencies.ts +4 -5
  49. package/server-managed/team-reports.ts +19 -7
  50. package/out/chunk-6S2U2UYG.js +0 -232
  51. package/out/chunk-FC65TPUB.js +0 -1
@@ -16,19 +16,17 @@
16
16
  // an agent, and those alone are answered, never the texts. GraphQL alone
17
17
  // gives them, so it needs a token.
18
18
  import { type AgentAuthor, type AgentBranch, type AgentCommit, type AgentIssue, type AgentPull, type AgentSignals, type AppAgent, agentSignals } from '../common/managed/agent-tasks.ts'
19
- import { GITHUB_AGENTS, type GithubAgentAccount, type GithubAgentId } from '../common/managed/github-agents.ts'
19
+ import { GITHUB_AGENTS, type GithubAgentAccount, type GithubAgentId, githubAgent } from '../common/managed/github-agents.ts'
20
20
  import { AGENT_BRANCH_PREFIXES, APP_AGENTS } from './app-agents.ts'
21
- import { GithubApiError } from './github-app.ts'
22
21
  import type { GraphqlError } from './github-graphql.ts'
23
22
  import { ACTOR, ALL_BRANCHES, type HistoryCommit, type HistoryPages, type NamedAccount, actorOf, commitAccount, commitMarks, githubGraphql, historyFrom, historyMarks,
24
23
  inBatches, linkNoreplyUsers, messageMarks, variablesOf } from './repository-history.ts'
25
- import type { PullList } from './repository-pulls.ts'
24
+ import type { IssueList, PullList } from './repository-pulls.ts'
26
25
 
27
26
  export const AGENT_PULLS_PAGE = 50
28
27
  export const AGENT_COMMITS_PAGE = 100
29
28
  const AGENT_ISSUES = 100
30
29
  const AGENT_BRANCHES = 500
31
- const MAX_BRANCH_PAGES = 50
32
30
  const CLOSES = 5
33
31
  // A pull request's labels read, for one naming an agent: GitHub's most a
34
32
  // page.
@@ -46,25 +44,36 @@ const PULLED_PARALLEL = 4
46
44
  const AGENT_EMAILS = APP_AGENTS.flatMap(({ id }) => GITHUB_AGENTS[id as GithubAgentId].accounts
47
45
  .flatMap(({ id: account, logins }: GithubAgentAccount) => account === undefined ? [] : logins.map(login => `${account}+${login}@users.noreply.github.com`)))
48
46
 
49
- // An agent's branches, a page at a time, as GitHub lists them, by name;
50
- // each its tip commit's time, message and author.
47
+ // Branches' tip commits, `c<n>` the `n`th's: each's message and author.
48
+ const tipsQuery = (count: number) => `query($owner: String!, $name: String!, ${variablesOf('o', count, 'GitObjectID!')}) {
49
+ repository(owner: $owner, name: $name) {
50
+ ${Array.from({ length: count }, (_, at) => `c${at}: object(oid: $o${at}) { ... on Commit { message author { email user { login databaseId } } } }`).join('\n ')}
51
+ }
52
+ }`
53
+ // Tips looked up a query, and queries at once.
54
+ const TIPS = 100
55
+ const TIPS_PARALLEL = 4
56
+ // An agent's branches, a page at a time, as GitHub lists them by name, where
57
+ // the repository's branches as kept stop short of them (`cut`): each its tip
58
+ // and when that was committed; as many pages of each as MAX_BRANCH_PAGES.
51
59
  const BRANCHES_QUERY = `query($owner: String!, $name: String!, $prefix: String!, $after: String) {
52
60
  repository(owner: $owner, name: $name) {
53
61
  refs(refPrefix: $prefix, first: 100, after: $after) {
54
62
  pageInfo { hasNextPage endCursor }
55
- nodes { prefix name target { ... on Commit { committedDate message author { email user { login databaseId } } } } }
63
+ nodes { prefix name target { oid ... on Commit { committedDate } } }
56
64
  }
57
65
  }
58
66
  }`
67
+ const MAX_BRANCH_PAGES = 50
59
68
  // What a commit of the default branch is read by.
60
69
  const COMMIT_FIELDS = 'oid messageHeadline message committedDate additions deletions author { email user { login databaseId } } associatedPullRequests(first: 1) { totalCount }'
61
- // The newest of the default branch's commits the app agents' accounts
62
- // authored, by their GitHub no-reply addresses, however far back: a bot's
63
- // commits pushed with no pull request here (as before a repository moved)
64
- // may lie deeper than pages are read on to.
65
- const AUTHORED_QUERY = `query($owner: String!, $name: String!, $emails: [String!]!) {
70
+ // The newest of the commits from the default branch's head the app agents'
71
+ // accounts authored, by their GitHub no-reply addresses, however far back: a
72
+ // bot's commits pushed with no pull request here (as before a repository
73
+ // moved) may lie deeper than pages are read on to.
74
+ const AUTHORED_QUERY = `query($owner: String!, $name: String!, $head: GitObjectID!, $emails: [String!]!) {
66
75
  repository(owner: $owner, name: $name) {
67
- defaultBranchRef { target { ... on Commit { history(first: ${AGENT_COMMITS_PAGE}, author: { emails: $emails }) { nodes { ${COMMIT_FIELDS} } } } } }
76
+ object(oid: $head) { ... on Commit { history(first: ${AGENT_COMMITS_PAGE}, author: { emails: $emails }) { nodes { ${COMMIT_FIELDS} } } } }
68
77
  }
69
78
  }`
70
79
  // What a pull request is read by, for its row (pull): its texts for the
@@ -74,13 +83,12 @@ const AUTHORED_QUERY = `query($owner: String!, $name: String!, $emails: [String!
74
83
  export const AGENT_PULL_FIELDS = `number title state isDraft createdAt updatedAt author { __typename ${ACTOR} } body headRefName isCrossRepository
75
84
  headRepositoryOwner { login } additions deletions last: commits(last: 1) { nodes { commit { message } } } mergeCommit { message }
76
85
  closingIssuesReferences(first: ${CLOSES}) { nodes { number } } labels(first: ${LABELS}) { nodes { name } }`
77
- // On the first page, the default branch's head and the latest issues.
78
- const PAGE_QUERY = `query($owner: String!, $name: String!) {
86
+ // What an issue is read by, for its row (issue).
87
+ export const AGENT_ISSUE_FIELDS = `number title state createdAt updatedAt author { __typename ${ACTOR} }`
88
+ // The latest issues, however long ago they were updated.
89
+ const ISSUES_QUERY = `query($owner: String!, $name: String!) {
79
90
  repository(owner: $owner, name: $name) {
80
- defaultBranchRef { target { oid } }
81
- issues(first: ${AGENT_ISSUES}, orderBy: { field: UPDATED_AT, direction: DESC }) {
82
- nodes { number title state createdAt updatedAt author { ${ACTOR} } }
83
- }
91
+ issues(first: ${AGENT_ISSUES}, orderBy: { field: UPDATED_AT, direction: DESC }) { nodes { ${AGENT_ISSUE_FIELDS} } }
84
92
  }
85
93
  }`
86
94
  // A page of the pull requests a search finds, as GitHub sorts them, on from
@@ -99,8 +107,7 @@ export type PullNode = { number?: unknown; title?: unknown; state?: unknown; isD
99
107
  type GitActor = { email?: unknown; user?: Login }
100
108
  type CommitNode = { oid?: unknown; messageHeadline?: unknown; message?: unknown; committedDate?: unknown; additions?: unknown; deletions?: unknown
101
109
  author?: GitActor | null; associatedPullRequests?: { totalCount?: unknown } }
102
- type IssueNode = { number?: unknown; title?: unknown; state?: unknown; createdAt?: unknown; updatedAt?: unknown; author?: Login }
103
- type RefNode = { prefix?: unknown; name?: unknown; target?: { committedDate?: unknown; message?: unknown; author?: GitActor | null } | null }
110
+ export type IssueNode = { number?: unknown; title?: unknown; state?: unknown; createdAt?: unknown; updatedAt?: unknown; author?: Login }
104
111
  type Connection<T> = { pageInfo?: { hasNextPage?: unknown; endCursor?: unknown }; nodes?: (T | null)[] }
105
112
  // Whether a pull request brought each commit, `c<n>` the `n`th's.
106
113
  const pulledQuery = (count: number) => `query($owner: String!, $name: String!, ${variablesOf('o', count, 'GitObjectID!')}) {
@@ -109,10 +116,12 @@ const pulledQuery = (count: number) => `query($owner: String!, $name: String!, $
109
116
  }
110
117
  }`
111
118
  type Pulled = { repository?: Record<string, { associatedPullRequests?: { totalCount?: unknown } } | null> | null }
112
- type Page = { repository?: { defaultBranchRef?: { target?: { oid?: unknown; history?: Connection<CommitNode> } | null } | null
113
- issues?: Connection<IssueNode> } | null }
114
- type Found = { search?: { pageInfo?: { endCursor?: unknown }; nodes?: (PullNode | null)[] } | null } | null
119
+ type Authored = { repository?: { object?: { history?: Connection<CommitNode> } | null } | null }
120
+ type Issues = { repository?: { issues?: Connection<IssueNode> } | null }
121
+ type Tips = { repository?: Record<string, { message?: unknown; author?: GitActor | null } | null> | null }
122
+ type RefNode = { prefix?: unknown; name?: unknown; target?: { oid?: unknown; committedDate?: unknown } | null }
115
123
  type RefsPage = { repository?: { refs?: Connection<RefNode> } | null }
124
+ type Found = { search?: { pageInfo?: { endCursor?: unknown }; nodes?: (PullNode | null)[] } | null } | null
116
125
 
117
126
  const count = (value: unknown) => Number.isSafeInteger(value) && (value as number) >= 0 ? value as number : 0
118
127
  const number = (value: unknown) => Number.isSafeInteger(value) && (value as number) > 0 ? value as number : null
@@ -147,12 +156,17 @@ const accountOf = (actor: Login) => both(actorOf(actor))
147
156
  // Dependabot's, is told by that alone (repository-history.ts commitAccount).
148
157
  const authorOf = (actor: GitActor | null | undefined) => both(commitAccount(actor))
149
158
 
159
+ // A commit the app agents' accounts authored, as kept: as read, its
160
+ // message's marks (messageMarks) in its place, never the message.
161
+ export type AuthoredCommit = Omit<CommitNode, 'message'> & { marks: AgentSignals | null }
162
+ const authoredOf = ({ message, ...node }: CommitNode): AuthoredCommit => ({ ...node, marks: messageMarks(message) })
163
+
150
164
  // A commit to the default branch no pull request merged, where an agent left
151
165
  // its marks on it, or Claude authored it (commitMarks).
152
- function directCommit(node: CommitNode | null): AgentCommit | null {
166
+ function directCommit(node: AuthoredCommit | null): AgentCommit | null {
153
167
  if (!node || typeof node.oid !== 'string' || !SHA.test(node.oid) || count(node.associatedPullRequests?.totalCount) > 0) return null
154
168
  const author = authorOf(node.author)
155
- const signals = commitMarks(messageMarks(node.message), { name: null, email: node.author?.email, account: commitAccount(node.author) })
169
+ const signals = commitMarks(node.marks, { name: null, email: node.author?.email, account: commitAccount(node.author) })
156
170
  if (!signals) return null
157
171
  return { oid: node.oid, title: nameOf(node.messageHeadline, MAX_TITLE) ?? '', at: timeOf(node.committedDate),
158
172
  author: author?.login ?? loginOf(node.author?.user), authorId: author?.id ?? null,
@@ -209,38 +223,96 @@ function frontier(commits: HistoryCommit[], left: string[]) {
209
223
  return starts.length > 0 ? starts.join(',') : null
210
224
  }
211
225
 
212
- function issue(node: IssueNode | null): AgentIssue | null {
226
+ // An issue's row: what it is, and who opened it.
227
+ export function issue(node: IssueNode | null): AgentIssue | null {
213
228
  const at = number(node?.number)
214
229
  if (!node || !at) return null
215
230
  return { number: at, title: nameOf(node.title, MAX_TITLE) ?? '', state: node.state === 'OPEN' ? 'open' : 'closed', author: loginOf(node.author),
216
231
  authorId: accountOf(node.author)?.id ?? null, createdAt: timeOf(node.createdAt), updatedAt: timeOf(node.updatedAt) }
217
232
  }
218
233
 
219
- // A branch under `prefix`, an agent's, named in full however GitHub splits
220
- // its name from the prefix it was listed under.
221
- const branch = (prefix: string) => (node: RefNode | null): AgentBranch | null => {
222
- const ref = typeof node?.prefix === 'string' && typeof node.name === 'string' ? `${node.prefix}${node.name}` : null
223
- const name = ref?.startsWith('refs/heads/') ? nameOf(ref.slice('refs/heads/'.length)) : null
224
- const author = authorOf(node?.target?.author)
225
- return name?.startsWith(prefix) ? { name, at: timeOf(node?.target?.committedDate), author: author?.login ?? loginOf(node?.target?.author?.user),
226
- authorId: author?.id ?? null, signals: agentSignals([node?.target?.message], APP_AGENTS, name, author) } : null
234
+ // A branch's tip, as kept: its message's marks (messageMarks), never the
235
+ // message, and its author as read.
236
+ export interface BranchTip { marks: AgentSignals | null; author: GitActor | null }
237
+ // The tips of branches, kept by their commits, which never change.
238
+ export interface BranchTips {
239
+ read(shas: string[]): Promise<Map<string, BranchTip>>
240
+ write(sha: string, tip: BranchTip): Promise<void>
241
+ }
242
+
243
+ // A branch an agent's prefix names (`name`), its tip (`sha`) committed `at`,
244
+ // as the Agents tab shows it: its tip's author, and the marks an agent left
245
+ // on it, its tip's message's, its author's account an agent's, or its name
246
+ // under an agent's prefix, as agent-tasks.ts agentSignals tells them.
247
+ function agentBranch({ name, at }: { name: string; at: number | null }, tip: BranchTip | undefined): AgentBranch {
248
+ const author = authorOf(tip?.author)
249
+ const authored = author ? githubAgent(author.login, author.id) : null
250
+ const marks = tip?.marks
251
+ return { name, at, author: author?.login ?? loginOf(tip?.author?.user), authorId: author?.id ?? null, signals: { sessions: marks?.sessions ?? [],
252
+ tasks: marks?.tasks ?? [], claude: marks?.claude ?? false, codex: marks?.codex ?? false,
253
+ agents: APP_AGENTS.flatMap(({ id, prefix }) => marks?.agents.includes(id) || authored === id || name.startsWith(prefix) ? [id] : []) } }
227
254
  }
228
255
 
229
- // The newest of an agent's branches, those under `prefix`: every one, as many
230
- // as MAX_BRANCH_PAGES of them list, kept by their tips' times.
231
- async function agentBranches(owner: string, name: string, prefix: string, token: string, fetchImpl: typeof fetch) {
232
- const found: AgentBranch[] = []
256
+ // The branches under `prefix`, every one, as many as MAX_BRANCH_PAGES of
257
+ // them list, named in full however GitHub splits its name from the prefix.
258
+ async function prefixHeads(owner: string, name: string, prefix: string, token: string, fetchImpl: typeof fetch) {
259
+ const found: Head[] = []
233
260
  let after: string | null = null
234
261
  for (let page = 0; page < MAX_BRANCH_PAGES; page++) {
235
262
  const refs: Connection<RefNode> | undefined = (await githubGraphql(BRANCHES_QUERY, { owner, name, prefix: `refs/heads/${prefix}`, after }, token, fetchImpl) as RefsPage)
236
263
  ?.repository?.refs
237
- await linkNoreplyUsers((refs?.nodes ?? []).map(node => node?.target?.author), token, fetchImpl)
238
- found.push(...(refs?.nodes ?? []).flatMap(node => branch(prefix)(node) ?? []))
264
+ for (const node of refs?.nodes ?? []) {
265
+ const ref = typeof node?.prefix === 'string' && typeof node.name === 'string' ? `${node.prefix}${node.name}` : null
266
+ const branch = ref?.startsWith('refs/heads/') ? nameOf(ref.slice('refs/heads/'.length)) : null
267
+ const sha = node?.target?.oid
268
+ if (branch?.startsWith(prefix) && typeof sha === 'string' && SHA.test(sha)) found.push({ name: branch, sha, at: timeOf(node!.target!.committedDate) })
269
+ }
239
270
  const next: Connection<RefNode>['pageInfo'] = refs?.pageInfo
240
271
  if (next?.hasNextPage !== true || typeof next.endCursor !== 'string') break
241
272
  after = next.endCursor
242
273
  }
243
- return found.toSorted((a, b) => (b.at ?? 0) - (a.at ?? 0)).slice(0, AGENT_BRANCHES)
274
+ return found
275
+ }
276
+
277
+ // The newest of the agents' branches, those each prefix names, as many as
278
+ // AGENT_BRANCHES of each, of the repository's branches as kept (`heads`, as
279
+ // the Commits tab reads them; those it stops short of, `cut`, listed by each
280
+ // prefix), their tips as kept (`tips`), those not looked up, TIPS a query,
281
+ // and kept.
282
+ async function agentBranches(fullName: string, token: string, { heads, cut }: { heads: Head[]; cut: boolean }, tips: BranchTips, fetchImpl: typeof fetch) {
283
+ const [owner, name] = fullName.split('/') as [string, string]
284
+ const all = cut ? (await Promise.all(AGENT_BRANCH_PREFIXES.map(prefix => prefixHeads(owner, name, prefix, token, fetchImpl)))).flat() : heads
285
+ const named = AGENT_BRANCH_PREFIXES.flatMap(prefix => all.filter(head => head.name.startsWith(prefix))
286
+ .toSorted((a, b) => (b.at ?? 0) - (a.at ?? 0)).slice(0, AGENT_BRANCHES))
287
+ const shown = named.filter((head, index) => named.findIndex(other => other.name === head.name) === index)
288
+ const shas = [...new Set(shown.map(head => head.sha))]
289
+ const known = await tips.read(shas)
290
+ await inBatches(shas.filter(sha => !known.has(sha)), TIPS, TIPS_PARALLEL, async batch => {
291
+ const data = await githubGraphql(tipsQuery(batch.length), { owner, name, ...Object.fromEntries(batch.map((sha, at) => [`o${at}`, sha])) }, token, fetchImpl,
292
+ notFound) as Tips
293
+ await linkNoreplyUsers(batch.map((_, at) => data?.repository?.[`c${at}`]?.author), token, fetchImpl)
294
+ await Promise.all(batch.map((sha, at) => {
295
+ const node = data?.repository?.[`c${at}`]
296
+ if (!node) return Promise.resolve()
297
+ known.set(sha, { marks: messageMarks(node.message), author: node.author ?? null })
298
+ return tips.write(sha, known.get(sha)!)
299
+ }))
300
+ })
301
+ return shown.map(head => agentBranch(head, known.get(head.sha)))
302
+ }
303
+
304
+ // The commits from `head` the app agents' accounts authored (AUTHORED_QUERY),
305
+ // as kept by it (`kept`), else read and kept.
306
+ async function authoredCommits(fullName: string, token: string, head: string, kept: AuthoredCommits, fetchImpl: typeof fetch) {
307
+ const [owner, name] = fullName.split('/') as [string, string]
308
+ const key = `${head}\0${AGENT_EMAILS.join(',')}`
309
+ const known = await kept.read(key)
310
+ if (known) return known
311
+ const data = await githubGraphql(AUTHORED_QUERY, { owner, name, head, emails: AGENT_EMAILS }, token, fetchImpl) as Authored
312
+ const nodes = (data?.repository?.object?.history?.nodes ?? []).flatMap(node => node ? [authoredOf(node)] : [])
313
+ await linkNoreplyUsers(nodes.map(node => node.author), token, fetchImpl)
314
+ await kept.write(key, nodes)
315
+ return nodes
244
316
  }
245
317
 
246
318
  // What its answers are kept by, with their cursors (http.ts
@@ -269,17 +341,31 @@ const positionOf = (item: AgentPull) => `${item.updatedAt ?? 0}.${item.number}`
269
341
  // where it names none.
270
342
  const updatedBy = (at: number) => Number.isFinite(at) ? ` updated:<=${new Date(at).toISOString().replace(/\.\d{3}Z$/u, '+00:00')}` : ''
271
343
 
272
- // The repository's pull requests as kept (repository-pulls.ts PullList),
344
+ // A branch: its name, its tip, and when that was committed.
345
+ type Head = { name: string; sha: string; at: number | null }
346
+ // The commits the app agents' accounts authored from a head (AUTHORED_QUERY),
347
+ // kept by it and the addresses they were asked by (`key`).
348
+ export interface AuthoredCommits {
349
+ read(key: string): Promise<AuthoredCommit[] | null>
350
+ write(key: string, commits: AuthoredCommit[]): Promise<void>
351
+ }
352
+ // What the repository's tabs read and keep together (http.ts): its pull
353
+ // requests and its issues (repository-pulls.ts PullList, IssueList), each
273
354
  // read as far back as their first page where none are, and those updated
274
- // since read as they are (http.ts keptPulls).
275
- export type KeptPulls = () => Promise<PullList>
355
+ // since read as they are (keptList), its default branch's head (defaultTip),
356
+ // its branches, as the Commits tab reads them (`heads`, whether they stop
357
+ // short of some, `cut`), their tips (`tips`), and the commits its app
358
+ // agents' accounts authored (`authored`).
359
+ export interface Kept { pulls(): Promise<PullList>; issues(): Promise<IssueList>; head(): Promise<string | null>; heads(): Promise<{ heads: Head[]; cut: boolean }>
360
+ tips: BranchTips
361
+ authored: AuthoredCommits }
276
362
 
277
363
  // A page of the repository's pull requests, the most recently updated
278
364
  // first, below the one `cursor` names (none: from the newest): of those kept
279
365
  // (`kept`), as far back as every one is read; past them, as GitHub's search
280
366
  // finds those updated before, a page a query. With where it reads on from,
281
367
  // null where none are left: past every one kept where all are.
282
- async function agentPulls(fullName: string, token: string, cursor: string | null, kept: KeptPulls, fetchImpl: typeof fetch) {
368
+ async function agentPulls(fullName: string, token: string, cursor: string | null, kept: Kept['pulls'], fetchImpl: typeof fetch) {
283
369
  const at = cursor === null ? null : POSITION.exec(cursor)
284
370
  const [time, past] = at ? [Number(at[1]), Number(at[2])] : [Number.POSITIVE_INFINITY, 0]
285
371
  const below = (item: AgentPull) => (item.updatedAt ?? 0) < time || ((item.updatedAt ?? 0) === time && item.number < past)
@@ -290,7 +376,7 @@ async function agentPulls(fullName: string, token: string, cursor: string | null
290
376
  const read = list.next ? list.next.to ?? Number.POSITIVE_INFINITY : list.reach ?? Number.NEGATIVE_INFINITY
291
377
  // As far back as every one is read: the year's start, where all since are.
292
378
  const bottom = Math.max(read, list.since)
293
- const listed = list.pulls.filter(item => below(item) && (item.updatedAt ?? 0) >= bottom)
379
+ const listed = list.rows.filter(item => below(item) && (item.updatedAt ?? 0) >= bottom)
294
380
  const whole = list.all === true && read === Number.NEGATIVE_INFINITY
295
381
  if (listed.length > 0) {
296
382
  const page = listed.slice(0, AGENT_PULLS_PAGE)
@@ -311,43 +397,50 @@ async function agentPulls(fullName: string, token: string, cursor: string | null
311
397
  return { pulls: rows.filter(item => item.updatedAt !== time || item.number !== past), after: after === cursor ? null : after }
312
398
  }
313
399
 
400
+ // The repository's latest issues, as many as AGENT_ISSUES: of those kept
401
+ // (`kept`), where they hold as many, or every one the repository has; else
402
+ // read, as those kept hold none updated before the year they are read for.
403
+ async function latestIssues(fullName: string, token: string, kept: Kept['issues'], fetchImpl: typeof fetch): Promise<AgentIssue[]> {
404
+ const list = await kept()
405
+ if (list.rows.length >= AGENT_ISSUES || list.all) return list.rows.slice(0, AGENT_ISSUES).map(({ bot: _bot, closedAt: _closedAt, reason: _reason, ...row }) => row)
406
+ const [owner, name] = fullName.split('/') as [string, string]
407
+ const data = await githubGraphql(ISSUES_QUERY, { owner, name }, token, fetchImpl) as Issues
408
+ return (data?.repository?.issues?.nodes ?? []).flatMap(node => issue(node) ?? [])
409
+ }
410
+
314
411
  // A page of what the repository's agents left: its pull requests below the
315
412
  // one the cursor `pulls` names, as kept (`kept`, agentPulls), its default
316
413
  // branch's commits on from those `commits` names, as its history is kept
317
- // (`pages`, historyFrom: the first page from its head), whether a pull
318
- // request brought those marked kept (`pulled`) (the first page where
319
- // neither is named, with its issues, its
320
- // agents' branches and the commits the app agents' accounts authored, and
321
- // the app agents it may name), and the cursors reading on from each, null
322
- // where either is read whole.
414
+ // (`pages`, historyFrom: the first page from its head, as kept), whether a
415
+ // pull request brought those marked kept (`pulled`) (the first page where
416
+ // neither is named, with its latest issues, as kept, its agents' branches
417
+ // and the commits the app agents' accounts authored, and the app agents it
418
+ // may name), and the cursors reading on from each, null where either is
419
+ // read whole.
323
420
  export async function repositoryAgents(fullName: string, token: string, cursors: { pulls: string | null; commits: string | null }, pages: HistoryPages,
324
- pulled: CommitPulls, kept: KeptPulls, fetchImpl: typeof fetch = globalThis.fetch) {
325
- const [owner, name] = fullName.split('/') as [string, string]
421
+ pulled: CommitPulls, kept: Kept, fetchImpl: typeof fetch = globalThis.fetch) {
326
422
  const first = cursors.pulls === null && cursors.commits === null
327
- const [data, branches, authored, pulls] = await Promise.all([
328
- first ? githubGraphql(PAGE_QUERY, { owner, name }, token, fetchImpl) as Promise<Page> : null,
329
- first ? Promise.all(AGENT_BRANCH_PREFIXES.map(prefix => agentBranches(owner, name, prefix, token, fetchImpl))) : [],
330
- first ? githubGraphql(AUTHORED_QUERY, { owner, name, emails: AGENT_EMAILS }, token, fetchImpl) as Promise<Page> : null,
331
- first || cursors.pulls !== null ? agentPulls(fullName, token, cursors.pulls, kept, fetchImpl) : { pulls: [], after: null },
423
+ const tip = first ? kept.head() : Promise.resolve(null)
424
+ const [head, issues, branches, nodes, pulls] = await Promise.all([
425
+ tip,
426
+ first ? latestIssues(fullName, token, kept.issues, fetchImpl) : [],
427
+ first ? kept.heads().then(heads => agentBranches(fullName, token, heads, kept.tips, fetchImpl)) : [],
428
+ tip.then(sha => typeof sha === 'string' && SHA.test(sha) ? authoredCommits(fullName, token, sha, kept.authored, fetchImpl) : []),
429
+ first || cursors.pulls !== null ? agentPulls(fullName, token, cursors.pulls, kept.pulls, fetchImpl) : { pulls: [], after: null },
332
430
  ])
333
- const repository = data?.repository
334
- if (data !== null && !repository) throw new GithubApiError(404, 'github-not-found')
335
- const head = repository?.defaultBranchRef?.target?.oid
336
431
  const named = cursors.commits === null ? first && typeof head === 'string' && SHA.test(head) ? [head] : [] : startsOf(cursors.commits)
337
432
  const starts = named.slice(0, ALL_BRANCHES)
338
433
  const history = starts.length > 0 ? (await historyFrom(fullName, token, starts, pages, fetchImpl)).commits : []
339
434
  const marked = history.flatMap(commit => { const signals = historyMarks(commit); return signals ? [{ commit, signals }] : [] })
340
- const nodes = authored?.repository?.defaultBranchRef?.target?.history?.nodes ?? []
341
- const [brought] = await Promise.all([pulledCommits(fullName, token, marked.map(({ commit }) => commit.sha), pulled, fetchImpl),
342
- linkNoreplyUsers(nodes.map(node => node?.author), token, fetchImpl)])
435
+ const brought = await pulledCommits(fullName, token, marked.map(({ commit }) => commit.sha), pulled, fetchImpl)
343
436
  const commits = [...marked.flatMap(({ commit, signals }) => brought.has(commit.sha) ? [] : [keptCommit(commit, signals)]), ...nodes.flatMap(node => directCommit(node) ?? [])]
344
437
  const after = { pulls: pulls.after, commits: frontier(history, named.slice(ALL_BRANCHES)) }
345
438
  return {
346
- pulls: pulls.pulls.map(({ merged: _merged, ...row }: AgentPull & { merged?: unknown }) => row),
439
+ pulls: pulls.pulls.map(({ bot: _bot, merged: _merged, ...row }: AgentPull & { bot?: unknown; merged?: unknown }) => row),
347
440
  commits: commits.filter((commit, index) => commits.findIndex(other => other.oid === commit.oid) === index),
348
441
  ...first ? {
349
- issues: (repository?.issues?.nodes ?? []).flatMap(node => issue(node) ?? []),
350
- branches: branches.flat(),
442
+ issues,
443
+ branches,
351
444
  agents: APP_AGENTS.map(({ id, name: agent, avatar }): AppAgent => ({ id, name: agent, ...avatar ? { avatar } : {} })),
352
445
  } : {},
353
446
  more: after.pulls !== null || after.commits !== null, after,
@@ -23,8 +23,8 @@ export const SINCE_COMMIT = /^[a-f\d]{40}$/u
23
23
 
24
24
  export interface RepoDependencyReview { enabled: boolean; required: number; since: string | null; lockfiles: LockfileScope }
25
25
  // A review published of the version it is of: its package, version and
26
- // npm's integrity of it, the patch it is of (none here, the app reviewing no
27
- // patch yet), the version it was compared with (none for a whole package's),
26
+ // npm's integrity of it, the patch it is of as patched (its hash, as a
27
+ // lockfile names it), the version it was compared with (none for a whole package's),
28
28
  // who wrote it (`reviewer`, their id, else their login then), their login
29
29
  // as it was then (`login`), its verdict, whether it checked every file
30
30
  // (`full`), and when it was published.
@@ -81,14 +81,14 @@ async function publishedReviews(db: ManagedSql, repoId: number, versions: Review
81
81
  const asked = versions.slice(at, at + VERSIONS_AT_ONCE)
82
82
  const keys = new Set(asked.map(({ name, version }) => `${name}@${version}`))
83
83
  const [names, numbers] = [[...new Set(asked.map(({ name }) => name))], [...new Set(asked.map(({ version }) => version))]]
84
- const rows = (await db.prepare(`SELECT r.id, r.user_id AS userId, r.user_login AS userLogin, r.package, r.version, r.integrity,
84
+ const rows = (await db.prepare(`SELECT r.id, r.user_id AS userId, r.user_login AS userLogin, r.package, r.version, r.integrity, r.patch_hash AS patchHash,
85
85
  NULLIF(r.base_package, '') AS basePackage, NULLIF(r.base_version, '') AS baseVersion, r.files, r.status, r.published_at AS publishedAt
86
86
  FROM managed_dependency_review r
87
87
  WHERE r.ecosystem = 'npm' AND r.published_at IS NOT NULL AND r.revoked_at IS NULL AND r.status IS NOT NULL
88
88
  AND r.package IN (${names.map(() => '?').join(',')}) AND r.version IN (${numbers.map(() => '?').join(',')})
89
89
  AND EXISTS (SELECT 1 FROM managed_dependency_review_team rt JOIN managed_team t ON t.id = rt.team_id
90
90
  WHERE rt.review_id = r.id AND ${COUNTING_TEAM})`).all(...names, ...numbers, repoId) as
91
- { id: string; userId: string | null; userLogin: string; package: string; version: string; integrity: string
91
+ { id: string; userId: string | null; userLogin: string; package: string; version: string; integrity: string; patchHash: string
92
92
  basePackage: string | null; baseVersion: string | null; files: string | null; status: ReviewVerdict; publishedAt: number }[])
93
93
  .filter(row => keys.has(`${row.package}@${row.version}`))
94
94
  if (rows.length === 0) continue
@@ -105,7 +105,8 @@ async function publishedReviews(db: ManagedSql, repoId: number, versions: Review
105
105
  const files = Array.isArray(snapshot) ? snapshot : snapshot?.paths ?? null
106
106
  const nonText = Array.isArray(snapshot) ? null : snapshot?.nonTextFiles ?? []
107
107
  const full = summarizeReview(entries.get(row.id) ?? [], files, nonText).coverage === 'full'
108
- found.push({ id: row.id, package: row.package, version: row.version, integrity: row.integrity, basePackage: row.basePackage, baseVersion: row.baseVersion,
108
+ found.push({ id: row.id, package: row.package, version: row.version, integrity: row.integrity, ...row.patchHash ? { patchHash: row.patchHash } : {},
109
+ basePackage: row.basePackage, baseVersion: row.baseVersion,
109
110
  reviewer: row.userId ?? `login:${row.userLogin.toLowerCase()}`, login: row.userLogin, status: row.status, full, time: Number(row.publishedAt) })
110
111
  }
111
112
  }