@preventive/triage 1.0.0-alpha.47 → 1.0.0-alpha.49

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 (82) hide show
  1. package/client/dependency-paths.d.ts +2 -1
  2. package/client/dependency-paths.js +16 -4
  3. package/common/managed/agent-tasks.ts +12 -14
  4. package/common/managed/github-agents.ts +7 -6
  5. package/out/activity.js +4 -4
  6. package/out/{chunk-NOZJULWS.js → chunk-6LNQTKAV.js} +1 -1
  7. package/out/chunk-7VAWINQF.js +6 -0
  8. package/out/{chunk-5AHIQFFG.js → chunk-C2AVODGZ.js} +1 -1
  9. package/out/{chunk-I2MDGSQX.js → chunk-IO7JTZ6O.js} +1 -1
  10. package/out/chunk-JEAUAJVF.js +232 -0
  11. package/out/chunk-O6M574JI.js +1 -0
  12. package/out/{chunk-VL43BTSO.js → chunk-PBSOL2SL.js} +1 -1
  13. package/out/{chunk-ZCS4UYHI.js → chunk-PBZPYIG7.js} +1 -1
  14. package/out/{chunk-6WF3KNTL.js → chunk-SZD5CW5R.js} +2 -2
  15. package/out/{chunk-SPEFPML6.js → chunk-TQJDYHVE.js} +1 -1
  16. package/out/{chunk-UENXT2HK.js → chunk-ULIF7A7N.js} +1 -1
  17. package/out/{chunk-FH24VHSJ.js → chunk-WYLQWYLA.js} +1 -1
  18. package/out/client-managed-import.js +1 -1
  19. package/out/client-managed.js +26 -26
  20. package/out/compare.js +13 -13
  21. package/out/dialogs.js +17 -17
  22. package/out/graph.js +1 -1
  23. package/out/index.html +7 -7
  24. package/out/scan.js +1 -1
  25. package/out/terminal.js +214 -214
  26. package/out/view.css +1 -1
  27. package/out/view.js +15 -15
  28. package/package.json +8 -5
  29. package/risk/npm/score.js +1 -1
  30. package/server-managed/agent-merges.ts +38 -22
  31. package/server-managed/app-agents.ts +6 -8
  32. package/server-managed/cache-storage.ts +3 -2
  33. package/server-managed/commit-coauthors.ts +7 -2
  34. package/server-managed/commit-lockfiles.ts +22 -21
  35. package/server-managed/db-methods.ts +52 -38
  36. package/server-managed/db-neon.ts +21 -5
  37. package/server-managed/db.ts +2 -0
  38. package/server-managed/dependabot-access.ts +1 -1
  39. package/server-managed/dependency-review-comments.ts +7 -4
  40. package/server-managed/dependency-reviews.ts +71 -33
  41. package/server-managed/github-accounts.ts +28 -1
  42. package/server-managed/github-app.ts +27 -23
  43. package/server-managed/github-graphql.ts +39 -0
  44. package/server-managed/github-issue-links.ts +8 -1
  45. package/server-managed/github-metadata.ts +2 -2
  46. package/server-managed/http-response.ts +28 -10
  47. package/server-managed/http.ts +751 -273
  48. package/server-managed/index.ts +2 -2
  49. package/server-managed/managed-issues.ts +3 -0
  50. package/server-managed/npm-delta.ts +16 -0
  51. package/server-managed/npm-kept.ts +32 -5
  52. package/server-managed/npm-loads.ts +21 -13
  53. package/server-managed/npm-manifest.ts +1 -1
  54. package/server-managed/npm-packages.ts +13 -4
  55. package/server-managed/object-storage.ts +4 -3
  56. package/server-managed/public-workspace.ts +3 -3
  57. package/server-managed/pull-comment-reviews.ts +1 -1
  58. package/server-managed/pull-dependencies.ts +84 -36
  59. package/server-managed/pull-dependency-reviews.ts +28 -18
  60. package/server-managed/rename-diff-worker.js +1 -1
  61. package/server-managed/repository-agents.ts +9 -6
  62. package/server-managed/repository-browser.ts +13 -3
  63. package/server-managed/repository-code.ts +153 -7
  64. package/server-managed/repository-dependency-reviews.ts +12 -4
  65. package/server-managed/repository-history.ts +85 -152
  66. package/server-managed/repository-issues.ts +27 -13
  67. package/server-managed/repository-pulse.ts +31 -11
  68. package/server-managed/repository-squashes.ts +145 -0
  69. package/server-managed/request-timing.ts +63 -0
  70. package/server-managed/reviewed-dependencies.ts +3 -2
  71. package/server-managed/session.ts +34 -12
  72. package/server-managed/sql.ts +16 -8
  73. package/server-managed/storage-encryption.ts +16 -8
  74. package/server-managed/storage-maintenance.ts +5 -2
  75. package/server-managed/storage-payload.ts +18 -0
  76. package/server-managed/storage-stores.ts +3 -2
  77. package/server-managed/team-feed.ts +16 -11
  78. package/server-managed/team-npm-scopes.ts +29 -0
  79. package/server-managed/team-reports.ts +68 -16
  80. package/out/chunk-6ANSHRFQ.js +0 -6
  81. package/out/chunk-S6U2J62U.js +0 -232
  82. package/out/chunk-ZGNMDGSB.js +0 -1
@@ -1,6 +1,6 @@
1
1
  // Compare's diff with renamed names hidden (rename-diff.ts), worked out off the
2
2
  // server's event loop by @preventive/compare's renamedDiffs: the old side given
3
- // the new side's names, formatted again where that changes its layout alone,
3
+ // the new side's names, not formatted again (each line where the old side's was),
4
4
  // diffed with whitespace changes shown and hidden, and the blocks checked to
5
5
  // take it to the new side. `{ before, blocks, reason, reads, marks,
6
6
  // ignoringWhitespace }`: the old side as diffed, the change blocks, why the
@@ -11,13 +11,12 @@
11
11
  // an agent, and those alone are answered, never the texts. GraphQL alone
12
12
  // gives them, so it needs a token.
13
13
  import { type AgentAuthor, type AgentBranch, type AgentCommit, type AgentIssue, type AgentPull, type AppAgent, agentSignals } from '../common/managed/agent-tasks.ts'
14
+ import { GITHUB_AGENTS, type GithubAgentAccount, type GithubAgentId } from '../common/managed/github-agents.ts'
14
15
  import { AGENT_BRANCH_PREFIXES, APP_AGENTS } from './app-agents.ts'
15
16
  import { GithubApiError } from './github-app.ts'
16
- import { ACTOR, type NamedAccount, actorOf, commitAccount, githubGraphql } from './repository-history.ts'
17
+ import { ACTOR, type NamedAccount, actorOf, commitAccount, githubGraphql, linkNoreplyUsers } from './repository-history.ts'
17
18
  import { ISSUES_CURSOR } from './repository-issues.ts'
18
19
 
19
- export { APP_AGENTS }
20
-
21
20
  export const AGENT_PULLS_PAGE = 50
22
21
  export const AGENT_COMMITS_PAGE = 100
23
22
  const AGENT_ISSUES = 100
@@ -35,7 +34,8 @@ const SHA = /^[a-f\d]{40}$/u
35
34
  const CLAUDE_EMAIL = 'noreply@anthropic.com'
36
35
 
37
36
  // The app agents' accounts' no-reply addresses, by each login of theirs.
38
- const AGENT_EMAILS = APP_AGENTS.flatMap(({ account }) => account?.logins.map(login => `${account.id}+${login}@users.noreply.github.com`) ?? [])
37
+ const AGENT_EMAILS = APP_AGENTS.flatMap(({ id }) => GITHUB_AGENTS[id as GithubAgentId].accounts
38
+ .flatMap(({ id: account, logins }: GithubAgentAccount) => account === undefined ? [] : logins.map(login => `${account}+${login}@users.noreply.github.com`)))
39
39
 
40
40
  // An agent's branches, a page at a time, as GitHub lists them, by name;
41
41
  // each its tip commit's time, message and author.
@@ -164,6 +164,7 @@ async function agentBranches(owner: string, name: string, prefix: string, token:
164
164
  for (let page = 0; page < MAX_BRANCH_PAGES; page++) {
165
165
  const refs: Connection<RefNode> | undefined = (await githubGraphql(BRANCHES_QUERY, { owner, name, prefix: `refs/heads/${prefix}`, after }, token, fetchImpl) as RefsPage)
166
166
  ?.repository?.refs
167
+ await linkNoreplyUsers((refs?.nodes ?? []).map(node => node?.target?.author), token, fetchImpl)
167
168
  found.push(...(refs?.nodes ?? []).flatMap(node => branch(prefix)(node) ?? []))
168
169
  const next: Connection<RefNode>['pageInfo'] = refs?.pageInfo
169
170
  if (next?.hasNextPage !== true || typeof next.endCursor !== 'string') break
@@ -175,7 +176,7 @@ async function agentBranches(owner: string, name: string, prefix: string, token:
175
176
  // What its answers are kept by, with their cursors (http.ts
176
177
  // handleRepositoryAgents): bumped as what a page reads, or how it tells an
177
178
  // agent, changes, so that no answer an older server kept is sent for a day.
178
- export const AGENTS_READ_VERSION = 6
179
+ export const AGENTS_READ_VERSION = 7
179
180
 
180
181
  // A cursor of the default branch's commits: GitHub names one by a commit
181
182
  // and how many on from it, `<oid> <offset>`, a space between.
@@ -206,7 +207,9 @@ export async function repositoryAgents(fullName: string, token: string, cursors:
206
207
  if (!repository) throw new GithubApiError(404, 'github-not-found')
207
208
  const history = repository.defaultBranchRef?.target?.history
208
209
  const after = { pulls: nextCursor(repository.pullRequests), commits: nextCursor(history, COMMITS_CURSOR) }
209
- const commits = [...history?.nodes ?? [], ...authored?.repository?.defaultBranchRef?.target?.history?.nodes ?? []].flatMap(node => directCommit(node) ?? [])
210
+ const nodes = [...history?.nodes ?? [], ...authored?.repository?.defaultBranchRef?.target?.history?.nodes ?? []]
211
+ await linkNoreplyUsers(nodes.map(node => node?.author), token, fetchImpl)
212
+ const commits = nodes.flatMap(node => directCommit(node) ?? [])
210
213
  return {
211
214
  pulls: (repository.pullRequests?.nodes ?? []).flatMap(node => pull(node) ?? []),
212
215
  commits: commits.filter((commit, index) => commits.findIndex(other => other.oid === commit.oid) === index),
@@ -171,9 +171,16 @@ async function askAccess(repo: SelectedRepo, start: string | null, userToken: st
171
171
  return githubJson(base, token, fetchImpl)
172
172
  }
173
173
  }
174
- // A repository last known private asks who the user is as it asks for its
175
- // metadata, not after: its permission is asked of them.
176
- if (repo.private && userToken) getIdentity().catch(() => {})
174
+ // A repository last known private asks who the user is, and mints the
175
+ // installation token its permission is asked with, as it asks for its
176
+ // metadata, not after. Each is asked once a request (the token once for
177
+ // concurrent requests too, github-app.ts), so a failure is answered where
178
+ // it is awaited. One made public since mints a token it never waits for,
179
+ // kept for later requests.
180
+ if (repo.private && userToken) {
181
+ getIdentity().catch(() => {})
182
+ if (repo.installationId != null) installationToken().catch(() => {})
183
+ }
177
184
  // Stored visibility can be stale (or internal with private=false). Require
178
185
  // current, explicit public visibility or the caller's effective permission.
179
186
  const data = await metadata() as RepoMetadata
@@ -232,6 +239,9 @@ async function repositoryReader(repo: SelectedRepo, userToken: string | null, in
232
239
  // The user's permission level on it where the gate asked GitHub of it, as
233
240
  // for a private repository; null for a public one.
234
241
  permission: () => level,
242
+ // Its default branch as GitHub named it when access was first checked,
243
+ // `''` where it named none: the one whose head `commit('')` reads.
244
+ defaultBranch,
235
245
  async refs() {
236
246
  // Suggestions are bounded; the input also accepts any branch or tag name.
237
247
  const list = async (kind: string) => {
@@ -4,9 +4,14 @@
4
4
  // bundle builds keep theirs (getRepoTreeTarball), and its files as the npm
5
5
  // viewer reads a package's (npm-packages.ts npmFileRows). Readers asking for
6
6
  // the same directory meanwhile share its body, a load among the npm viewer's
7
- // active ones (npm-loads.ts sharedLoad).
7
+ // active ones (npm-loads.ts sharedLoad). The default branch's head's bodies
8
+ // are kept in byte storage, for the next to open them (repositoryCodeKept).
8
9
  import { Buffer } from 'node:buffer'
10
+ import { createHash } from 'node:crypto'
9
11
  import { encodeBrotli } from './brotli.ts'
12
+ import { CacheMissError, type CacheStorage } from './cache-storage.ts'
13
+ import type { KeptStore } from './npm-kept.ts'
14
+ import { plainObject } from './npm-manifest.ts'
10
15
  import { MAX_NPM_JSON_LENGTH, NpmPackageError, npmFileRows } from './npm-packages.ts'
11
16
  import { type NpmLoad, loadPlaces, readTarballFiles, sharedLoad } from './npm-loads.ts'
12
17
 
@@ -49,9 +54,14 @@ const directoryError = (err: unknown) => {
49
54
 
50
55
  // The directory as the viewer reads it, as brotli-encoded JSON: `{ repoId,
51
56
  // repo, commit, path, tree, files }`, its files as npmFileRows gives them,
52
- // by their paths in the directory, from `tarball`, the tree's.
53
- export function loadRepositoryCodeBody(input: RepositoryCodeInput, tarball: () => Promise<Uint8Array>): NpmLoad<Buffer> {
57
+ // by their paths in the directory, from `tarball`, the tree's, as upstream
58
+ // verifies it with its symlinks let climb `symlinksAbove` levels.
59
+ export function loadRepositoryCodeBody(input: RepositoryCodeInput, tarball: (symlinksAbove: number) => Promise<Uint8Array>): NpmLoad<Buffer> {
54
60
  const { repoId, repo, commit, path, tree, listed } = input
61
+ // A symlink may reach the repository's root, as a monorepo package's
62
+ // LICENSE links the root's, and no further: the directory's depth, less
63
+ // the level the archive's root already stands above its top directory.
64
+ const symlinksAbove = path.split('/').length - 1
55
65
  // Its places among the active loads, by its files as listed, hold their
56
66
  // JSON too, which the listing cannot tell, as escaping may grow a text
57
67
  // sixfold: as much as a package's may take for each.
@@ -59,11 +69,147 @@ export function loadRepositoryCodeBody(input: RepositoryCodeInput, tarball: () =
59
69
  const maxLength = Math.min(MAX_REPOSITORY_CODE_JSON_LENGTH, places * MAX_NPM_JSON_LENGTH)
60
70
  return sharedLoad(`repository ${repoId} ${repo} ${commit} ${path} ${tree}`, async () => {
61
71
  let files
62
- // A symlink may reach the repository's root, as a monorepo package's
63
- // LICENSE links the root's, and no further: the directory's depth, less
64
- // the level the archive's root already stands above its top directory.
65
- try { files = npmFileRows(await readTarballFiles(await tarball(), { ...REPOSITORY_TAR, symlinksAbove: path.split('/').length - 1 }), maxLength) }
72
+ try { files = npmFileRows(await readTarballFiles(await tarball(symlinksAbove), { ...REPOSITORY_TAR, symlinksAbove }), maxLength) }
66
73
  catch (err) { throw directoryError(err) }
67
74
  return encodeBrotli(Buffer.from(JSON.stringify({ repoId, repo, commit, path, tree, files })))
68
75
  }, 'repository-busy', { places })
69
76
  }
77
+
78
+ // The form of the bodies kept, bumped as what a body holds changes
79
+ // (npmFileRows): one kept in another is none to read.
80
+ const KEPT_CODE_VERSION = 1
81
+ // The largest body kept. Source text runs, as brotli's JSON, to a third of
82
+ // its bytes or less, and a directory's binary files are named, not held, so
83
+ // this keeps one of some 200 MiB of text: nearly every directory that opens
84
+ // at all (MAX_REPOSITORY_CODE_BYTES). One past it would take as much byte
85
+ // storage as hundreds of ordinary directories, and hold a serverless
86
+ // invocation a while uploading it: it is sent as ever, and read again the
87
+ // next time.
88
+ export const MAX_KEPT_CODE_BYTES = 64 * 1024 * 1024
89
+ const COMMIT = /^[a-f\d]{40}$/u
90
+
91
+ // A directory as kept: the repository by its id and name, the commit and the
92
+ // directory, as its body names them; and the head they are kept at.
93
+ export type KeptCodeAt = Pick<RepositoryCodeInput, 'repoId' | 'repo' | 'commit' | 'path'>
94
+ type KeptCodeHead = Pick<KeptCodeAt, 'commit' | 'repo'>
95
+ // Under the repository, then the commit, so either goes by its prefix; the
96
+ // repository's name and the directory hashed, as a key's parts are names.
97
+ const keptKey = ({ repoId, repo, commit, path }: KeptCodeAt) =>
98
+ `${repoId}/${commit}/v${KEPT_CODE_VERSION}-${createHash('sha256').update(JSON.stringify([repo, path])).digest('hex')}.json.br`
99
+
100
+ // Bodies being kept, by their storage and key, each kept once; and each
101
+ // storage's own name among the active loads, as its reads are one of them.
102
+ const keeping = new WeakMap<CacheStorage, Set<string>>()
103
+ const storageNames = new WeakMap<CacheStorage, number>()
104
+ let storagesNamed = 0
105
+
106
+ // A body kept, read whole, where it is one MAX_KEPT_CODE_BYTES keeps.
107
+ async function readKept(storage: CacheStorage, key: string): Promise<Buffer | null> {
108
+ const { size, stream } = await storage.open(key)
109
+ try {
110
+ if (size !== null && size > MAX_KEPT_CODE_BYTES) return null
111
+ const parts: Buffer[] = []
112
+ let length = 0
113
+ for await (const part of stream) {
114
+ length += (part as Buffer).byteLength
115
+ if (length > MAX_KEPT_CODE_BYTES) return null
116
+ parts.push(Buffer.from(part as Buffer))
117
+ }
118
+ return Buffer.concat(parts, length)
119
+ } finally { stream.destroy() }
120
+ }
121
+
122
+ // The directories of repositories' default branches' heads, as
123
+ // loadRepositoryCodeBody encodes them, kept in the managed byte storage
124
+ // (`storage`: Vercel Blob, else the filesystem, encrypted at rest where
125
+ // storage is: storage-payload.ts REPOSITORY_CODE_CACHE), so that one opened
126
+ // again, on any instance, asks GitHub for no tarball, and is neither unpacked
127
+ // nor encoded again; by the commit `heads` keeps for each repository, the
128
+ // head as last read (npm-kept.ts codeHeadStore). Most opens read the head:
129
+ // `/repo/<repo id>` opens it, and its route then names its commit, as do its
130
+ // directories opened from it and a reload. A read naming the default branch
131
+ // (`''`, or `heads/` and its name) takes its head as GitHub resolves it for
132
+ // the commit kept, the directories kept at any other removed (`own`ed,
133
+ // behind its answer); any other read, of a commit, a tag or another branch,
134
+ // reads and keeps a directory only where what it resolves to is the commit
135
+ // kept. Nothing kept is per reader, and each is read only once the reader's
136
+ // own access has named its commit. A body names what it was read of, so
137
+ // instances disagreeing on the head meanwhile, as at a push, only cost a
138
+ // directory read again: one kept for a head since moved on removes itself.
139
+ // The head is kept with the repository's name, which each body's key holds,
140
+ // so a repository renamed has what was kept under its name before removed
141
+ // as a read next names its head. A head is written only for a repository
142
+ // still `active` after it, as one removed meanwhile has its head removed
143
+ // after (remove). Byte storage, as the database, is an optimization: its
144
+ // failures read as a directory kept nowhere, and keep nothing.
145
+ export function repositoryCodeKept(storage: CacheStorage, heads: KeptStore<KeptCodeHead>, active: (repoId: number) => Promise<boolean>,
146
+ own: (work: Promise<unknown>) => void, debug = false) {
147
+ const warn = (what: string) => (error: unknown) => { if (debug) console.warn(`managed: kept repository code ${what} failed:`, error) }
148
+ const headOf = async (repoId: number): Promise<KeptCodeHead | null> => {
149
+ const kept: unknown = (await heads.read(String(repoId)))?.value
150
+ return plainObject(kept) && typeof kept['commit'] === 'string' && typeof kept['repo'] === 'string' ? { commit: kept['commit'], repo: kept['repo'] } : null
151
+ }
152
+ // What is kept of repository `repoId`: its head first, so that a body being
153
+ // kept meanwhile removes itself.
154
+ const remove = async (repoId: number) => {
155
+ await heads.remove(String(repoId))
156
+ await storage.delete(String(repoId))
157
+ }
158
+ return {
159
+ // Whether a read of `ref` at repository `repoId`, by its name `repo`,
160
+ // resolved to `commit` with `defaultBranch` GitHub's, reads and keeps
161
+ // its directories.
162
+ async at({ repoId, repo, commit }: Omit<KeptCodeAt, 'path'>, ref: string, defaultBranch: string): Promise<boolean> {
163
+ if (!COMMIT.test(commit)) return false
164
+ const kept = await headOf(repoId)
165
+ const atKept = kept?.commit === commit && kept.repo === repo
166
+ if (ref !== '' && (defaultBranch === '' || ref !== `heads/${defaultBranch}`)) return atKept
167
+ if (!atKept) {
168
+ await heads.write(String(repoId), { commit, repo }, Date.now())
169
+ // One removed as its commit was read, its head removed before this
170
+ // wrote it again, keeps nothing, and has what is kept removed again.
171
+ if (!await active(repoId).catch(() => false)) {
172
+ await remove(repoId).catch(warn('removal'))
173
+ return false
174
+ }
175
+ // Those kept at the head before, or, renamed, every one kept under
176
+ // its name before.
177
+ own(storage.delete(String(repoId), kept?.repo === repo ? commit : undefined).catch(warn('removal')))
178
+ }
179
+ return true
180
+ },
181
+ // The body kept of the directory `at` names, or null: a load among the
182
+ // active ones (npm-loads.ts sharedLoad), as one read from GitHub is, a
183
+ // place of them, as it holds no more than a package may
184
+ // (MAX_KEPT_CODE_BYTES), till each reader has released it; readers
185
+ // asking for it meanwhile share it, and none is read where every place
186
+ // is taken (`repository-busy`).
187
+ read(at: KeptCodeAt): NpmLoad<Buffer | null> {
188
+ const key = keptKey(at)
189
+ const name = storageNames.get(storage) ?? storageNames.set(storage, ++storagesNamed).get(storage)!
190
+ return sharedLoad(`repository kept ${name} ${key}`, () => readKept(storage, key).catch((error: unknown) => {
191
+ if (!(error instanceof CacheMissError)) warn('read')(error)
192
+ return null
193
+ }), 'repository-busy')
194
+ },
195
+ // The directory's body kept, behind its answer, where it is no larger
196
+ // than MAX_KEPT_CODE_BYTES; and removed again, with the rest of its
197
+ // commit's, where the head kept is another by then, as when it moved on
198
+ // or the repository was renamed or removed meanwhile.
199
+ keep(at: KeptCodeAt, body: Buffer): void {
200
+ const key = keptKey(at)
201
+ const kept = keeping.get(storage) ?? keeping.set(storage, new Set()).get(storage)!
202
+ if (body.byteLength > MAX_KEPT_CODE_BYTES || kept.has(key)) return
203
+ kept.add(key)
204
+ own((async () => {
205
+ await storage.put(key, body)
206
+ const head = await headOf(at.repoId)
207
+ if (head?.commit !== at.commit || head.repo !== at.repo) await storage.delete(`${at.repoId}/${at.commit}`)
208
+ })().catch(warn('write')).finally(() => kept.delete(key)))
209
+ },
210
+ // What is kept of repository `repoId`, removed once the repository is
211
+ // no longer active.
212
+ remove,
213
+ }
214
+ }
215
+ export type RepositoryCodeKept = ReturnType<typeof repositoryCodeKept>
@@ -25,13 +25,14 @@ export interface RepoDependencyReview { enabled: boolean; required: number; sinc
25
25
  // A review published of the version it is of: its package, version and
26
26
  // npm's integrity of it, the patch it is of (none here, the app reviewing no
27
27
  // patch yet), the version it was compared with (none for a whole package's),
28
- // who wrote it (`reviewer`, their id, else their login then), its verdict,
29
- // whether it checked every file (`full`), and when it was published.
28
+ // who wrote it (`reviewer`, their id, else their login then), their login
29
+ // as it was then (`login`), its verdict, whether it checked every file
30
+ // (`full`), and when it was published.
30
31
  // One made in a pull request's comments (`comment`) counts for it alone;
31
32
  // one published here has its `id`.
32
33
  export interface PublishedReview {
33
34
  id?: string; package: string; version: string; integrity: string; patchHash?: string; basePackage: string | null; baseVersion: string | null
34
- reviewer: string; status: ReviewVerdict; full: boolean; time: number; comment?: true
35
+ reviewer: string; login: string; status: ReviewVerdict; full: boolean; time: number; comment?: true
35
36
  }
36
37
  // A repository's review in effect for a reader: its settings, whether they
37
38
  // publish reviews to one of the teams that count (`canReview`), and the
@@ -45,6 +46,8 @@ export interface RepositoryDependencyReviewStore {
45
46
  listRepoDependencyReviews(): Promise<Map<number, RepoDependencyReview>>
46
47
  // One repository's, none where it isn't connected.
47
48
  repoDependencyReview(repoId: number): Promise<RepoDependencyReview | null>
49
+ // Those of `repoIds` connected, by their ids, in one statement.
50
+ getRepoDependencyReviews(repoIds: number[]): Promise<Map<number, RepoDependencyReview>>
48
51
  // The teams whose published reviews count for each repository (COUNTING_TEAM),
49
52
  // by name.
50
53
  listRepoReviewTeams(): Promise<Map<number, { id: string; name: string }[]>>
@@ -103,7 +106,7 @@ async function publishedReviews(db: ManagedSql, repoId: number, versions: Review
103
106
  const nonText = Array.isArray(snapshot) ? null : snapshot?.nonTextFiles ?? []
104
107
  const full = summarizeReview(entries.get(row.id) ?? [], files, nonText).coverage === 'full'
105
108
  found.push({ id: row.id, package: row.package, version: row.version, integrity: row.integrity, basePackage: row.basePackage, baseVersion: row.baseVersion,
106
- reviewer: row.userId ?? `login:${row.userLogin.toLowerCase()}`, status: row.status, full, time: Number(row.publishedAt) })
109
+ reviewer: row.userId ?? `login:${row.userLogin.toLowerCase()}`, login: row.userLogin, status: row.status, full, time: Number(row.publishedAt) })
107
110
  }
108
111
  }
109
112
  return found
@@ -122,6 +125,11 @@ export function repositoryDependencyReviewMethods(db: ManagedSql): RepositoryDep
122
125
  return new Map(rows.map(row => [row.repoId, setting(row)]))
123
126
  },
124
127
  repoDependencyReview: readSetting,
128
+ async getRepoDependencyReviews(repoIds) {
129
+ if (repoIds.length === 0) return new Map()
130
+ const rows = await db.prepare(`SELECT ${columns} FROM managed_selected_repo WHERE repo_id IN (${repoIds.map(() => '?').join(',')})`).all(...repoIds) as SettingRow[]
131
+ return new Map(rows.map(row => [row.repoId, setting(row)]))
132
+ },
125
133
  async listRepoReviewTeams() {
126
134
  const rows = await db.prepare(`SELECT DISTINCT tr.repo_id AS repoId, t.id AS id, t.name AS name FROM managed_team_repo tr
127
135
  JOIN managed_team t ON t.id = tr.team_id WHERE t.hidden = 0 AND t.dependency_review = 1 ORDER BY t.name, t.id`).all() as { repoId: number; id: string; name: string }[]