@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preventive/triage",
3
- "version": "1.0.0-alpha.47",
3
+ "version": "1.0.0-alpha.49",
4
4
  "description": "Client & relay server for triaging of automated reports",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "author": {
@@ -130,6 +130,7 @@
130
130
  "server-managed/github-actions.ts",
131
131
  "server-managed/github-app.ts",
132
132
  "server-managed/github-errors.ts",
133
+ "server-managed/github-graphql.ts",
133
134
  "server-managed/github-metadata.ts",
134
135
  "server-managed/github-issue-oauth.ts",
135
136
  "server-managed/github-issues.ts",
@@ -187,9 +188,11 @@
187
188
  "server-managed/repository-changes.ts",
188
189
  "server-managed/repository-code.ts",
189
190
  "server-managed/repository-dependency-reviews.ts",
191
+ "server-managed/request-timing.ts",
190
192
  "server-managed/reviewed-dependencies.ts",
191
193
  "server-managed/repository-agents.ts",
192
194
  "server-managed/repository-history.ts",
195
+ "server-managed/repository-squashes.ts",
193
196
  "server-managed/repository-issues.ts",
194
197
  "server-managed/repository-pulse.ts",
195
198
  "server-managed/repository-aliases.ts",
@@ -401,16 +404,16 @@
401
404
  "typescript": "^7.0.2"
402
405
  },
403
406
  "dependencies": {
404
- "@exodus/stasis": "1.0.0-beta.9",
405
- "@exodus/stasis-core": "1.0.0-beta.9",
406
- "@preventive/archive": "^1.1.0",
407
+ "@exodus/stasis": "1.0.0-beta.11",
408
+ "@exodus/stasis-core": "1.0.0-beta.11",
409
+ "@preventive/archive": "^1.2.0",
407
410
  "@preventive/compare": "^1.2.11",
408
411
  "@preventive/diff": "^1.1.0",
409
412
  "@preventive/lockfile": "^1.2.0",
410
413
  "@preventive/publishers": "^1.1.0",
411
414
  "@preventive/report": "^1.0.0",
412
415
  "@preventive/sourcemap": "1.0.0-alpha.1",
413
- "@preventive/upstream": "^1.2.0",
416
+ "@preventive/upstream": "^1.3.0",
414
417
  "css-tree": "^3.2.1",
415
418
  "oxc-minify": "^0.152.0",
416
419
  "oxc-parser": "^0.152.0",
package/risk/npm/score.js CHANGED
@@ -7,7 +7,7 @@ import { DAY_MS } from './downloads.js'
7
7
  // How much each finding raises the score, by its severity: a version's
8
8
  // findings combined as independent chances are (1 − Π(1 − weight)), so that
9
9
  // each adds less the more there are, none past 100. One red finding alone is
10
- // 50; two, or one with two orange, past 75, as attacks show.
10
+ // 50; two are 75, and one with two orange 68, as attacks show.
11
11
  const WEIGHTS = { critical: 0.5, high: 0.2, moderate: 0.08, low: 0.03, notice: 0 }
12
12
  // What the version's age tells instead (`too-new`), as exposure does below.
13
13
  const TOLD_BY_AGE = new Set(['too-new'])
@@ -4,10 +4,11 @@
4
4
  // pull request's branch alone naming it (app-agents.ts
5
5
  // AGENT_BRANCH_PREFIXES), or a label one who ran it gave it naming the agent
6
6
  // (agent-tasks.ts labelAgents), as the Agents tab tells an agent's work; or
7
- // its author, an account agents work as (github-agents.ts AGENT_OPERATED).
7
+ // its author, an account agents work as (github-agents.ts AGENT_OPERATED),
8
+ // or any GitHub App's bot.
8
9
  import { labelAgents } from '../common/managed/agent-tasks.ts'
9
10
  import { GITHUB_AGENTS, agentOperated } from '../common/managed/github-agents.ts'
10
- import { AGENT_BRANCH_PREFIXES, APP_AGENTS } from './app-agents.ts'
11
+ import { AGENT_BRANCH_PREFIXES } from './app-agents.ts'
11
12
  import { type HistoryCommit, githubGraphql } from './repository-history.ts'
12
13
 
13
14
  // The merged pull requests read an answer, a page a query, as many queries
@@ -22,7 +23,7 @@ const MERGED_LABELS = 100
22
23
  // How long before or after a pull request rebased onto the branch was merged
23
24
  // its copies are committed.
24
25
  const REBASE_SKEW_MS = 10 * 60_000
25
- const LABEL_AGENTS = [...new Set([...Object.keys(GITHUB_AGENTS), ...APP_AGENTS.map(agent => agent.id)])]
26
+ const LABEL_AGENTS = Object.keys(GITHUB_AGENTS)
26
27
  const SHA = /^[a-f\d]{40}$/u
27
28
 
28
29
  const count = (value: unknown) => Number.isSafeInteger(value) && (value as number) >= 0 ? value as number : 0
@@ -36,14 +37,18 @@ const timeOf = (value: unknown) => {
36
37
  // rebased onto the branch by their last commit there, when merged, and
37
38
  // their commits' subjects, the oldest first (agentCommits); when the newest
38
39
  // read was last updated, those updated since read again (`through`, null
39
- // till all are read once); and a read not finished: where it goes on
40
- // (`after`), when the newest it began with was updated (`from`), the oldest
41
- // it reads to (`stop`) and how many it has read (`read`). And of every pull
40
+ // till all are read once), and where those read reach back no further than
41
+ // a period needed, when the oldest of them was updated (`reach`), every
42
+ // pull request merged since read (one is updated when merged, or after);
43
+ // and a read not finished: where it goes on (`after`), when the newest it
44
+ // began with was updated (`from`), the oldest it reads to (`stop`), how many
45
+ // it has read (`read`) and when the oldest of them was updated (`to`). And
46
+ // of every pull
42
47
  // request merged, an agent's or not, its merge commit and when (`pulls`),
43
48
  // and one rebased so (`pullRebases`), for the commits pushed to the branch
44
49
  // directly to be told (directCommits).
45
- export interface AgentMerges { merges: [string, number][]; rebases: Rebase[]; through: number | null
46
- next: { after: string; from: number; stop: number; read: number } | null; pulls?: [string, number][]; pullRebases?: Rebase[] }
50
+ export interface AgentMerges { merges: [string, number][]; rebases: Rebase[]; through: number | null; reach?: number
51
+ next: { after: string; from: number; stop: number; read: number; to?: number } | null; pulls?: [string, number][]; pullRebases?: Rebase[] }
47
52
  // A pull request rebased onto the branch: its last commit there, when it was
48
53
  // merged, its latest commits' subjects, the oldest first, and how many more
49
54
  // it had than those read (`more`).
@@ -53,14 +58,14 @@ const MERGED_QUERY = `query($owner: String!, $name: String!, $after: String) {
53
58
  repository(owner: $owner, name: $name) {
54
59
  pullRequests(states: MERGED, first: ${MERGED_PAGE}, after: $after, orderBy: { field: UPDATED_AT, direction: DESC }) {
55
60
  pageInfo { hasNextPage endCursor }
56
- nodes { updatedAt mergedAt headRefName author { login ... on User { databaseId } } labels(first: ${MERGED_LABELS}) { nodes { name } }
61
+ nodes { updatedAt mergedAt headRefName author { __typename login ... on User { databaseId } } labels(first: ${MERGED_LABELS}) { nodes { name } }
57
62
  mergeCommit { oid parents { totalCount } }
58
63
  commits(last: ${MERGED_COMMITS}) { totalCount nodes { commit { oid messageHeadline changedFilesIfAvailable } } } }
59
64
  }
60
65
  }
61
66
  }`
62
67
  type MergedPage = { repository?: { pullRequests?: { pageInfo?: { hasNextPage?: unknown; endCursor?: unknown }
63
- nodes?: ({ updatedAt?: unknown; mergedAt?: unknown; headRefName?: unknown; author?: { login?: unknown; databaseId?: unknown } | null
68
+ nodes?: ({ updatedAt?: unknown; mergedAt?: unknown; headRefName?: unknown; author?: { __typename?: unknown; login?: unknown; databaseId?: unknown } | null
64
69
  labels?: { nodes?: ({ name?: unknown } | null)[] } | null
65
70
  mergeCommit?: { oid?: unknown; parents?: { totalCount?: unknown } | null } | null
66
71
  commits?: { totalCount?: unknown; nodes?: ({ commit?: { oid?: unknown; messageHeadline?: unknown; changedFilesIfAvailable?: unknown } | null } | null)[] } | null
@@ -68,9 +73,10 @@ type MergedPage = { repository?: { pullRequests?: { pageInfo?: { hasNextPage?: u
68
73
  const agentBranch = (name: unknown) => typeof name === 'string' && AGENT_BRANCH_PREFIXES.some(prefix => name.startsWith(prefix))
69
74
  type MergedNode = NonNullable<NonNullable<NonNullable<NonNullable<MergedPage['repository']>['pullRequests']>['nodes']>[number]>
70
75
  // Whether a pull request is an agent's: by its branch, a label naming one,
71
- // or its author, an account agents work as.
76
+ // or its author, an account agents work as or a bot (GitHub's GraphQL API
77
+ // naming a bot by its app's login alone, its type telling it).
72
78
  const agentPull = ({ headRefName, labels, author }: MergedNode) => agentBranch(headRefName)
73
- || labelAgents((labels?.nodes ?? []).map(label => label?.name), LABEL_AGENTS).length > 0
79
+ || labelAgents((labels?.nodes ?? []).map(label => label?.name), LABEL_AGENTS).length > 0 || author?.__typename === 'Bot'
74
80
  || agentOperated(typeof author?.login === 'string' ? author.login : null, Number.isSafeInteger(author?.databaseId) ? author!.databaseId as number : null)
75
81
 
76
82
  // The commits the pull requests merged since `since` from an agent's branch,
@@ -84,20 +90,24 @@ const agentPull = ({ headRefName, labels, author }: MergedNode) => agentBranch(h
84
90
  // first (a pull request is updated when merged, or after), on from what was
85
91
  // `kept`: till one updated before the year, or before the newest read before
86
92
  // was, as many queries an answer as MERGED_QUERIES, a read not finished
87
- // going on from where it was left by the next; one read again whose label
88
- // no longer names an agent, what it was read to bring forgotten.
93
+ // going on from where it was left by the next, or once those merged since
94
+ // `need` are all read; one read again whose label no longer names an agent,
95
+ // what it was read to bring forgotten.
89
96
  export async function agentMerges(fullName: string, token: string, since: number, kept: AgentMerges | null,
90
- fetchImpl: typeof fetch = globalThis.fetch): Promise<AgentMerges> {
97
+ fetchImpl: typeof fetch = globalThis.fetch, need = since): Promise<AgentMerges> {
91
98
  const [owner, name] = fullName.split('/') as [string, string]
92
99
  const merges = new Map((kept?.merges ?? []).filter(([, at]) => at >= since))
93
100
  const rebases = new Map((kept?.rebases ?? []).filter(([, at]) => at >= since).map(([sha, at, subjects, more = 0]) => [sha, [at, subjects, more] as const]))
94
101
  const pulls = new Map((kept?.pulls ?? []).filter(([, at]) => at >= since))
95
102
  const pullRebases = new Map((kept?.pullRebases ?? []).filter(([, at]) => at >= since).map(([sha, at, subjects, more = 0]) => [sha, [at, subjects, more] as const]))
96
- const stop = kept?.next?.stop ?? Math.max(since, kept?.through ?? since)
103
+ // Those read reaching back no further than they needed, and `need`
104
+ // further: read again from the newest on, as far back as it needs.
105
+ const further = !kept?.next && kept?.reach !== undefined && kept.reach >= need
106
+ const stop = kept?.next?.stop ?? (further ? since : Math.max(since, kept?.through ?? since))
97
107
  const listed = (map: typeof rebases) => [...map].map(([sha, [at, subjects, more]]): Rebase => more > 0 ? [sha, at, subjects, more] : [sha, at, subjects])
98
- const result = (through: number | null, next: AgentMerges['next']): AgentMerges => ({ merges: [...merges], rebases: listed(rebases), through, next,
99
- pulls: [...pulls], pullRebases: listed(pullRebases) })
100
- let { after = null, from = null, read = 0 }: { after?: string | null; from?: number | null; read?: number } = kept?.next ?? {}
108
+ const result = (through: number | null, next: AgentMerges['next'], reach?: number): AgentMerges => ({ merges: [...merges], rebases: listed(rebases), through,
109
+ ...reach === undefined ? {} : { reach }, next, pulls: [...pulls], pullRebases: listed(pullRebases) })
110
+ let { after = null, from = null, read = 0, to = null }: { after?: string | null; from?: number | null; read?: number; to?: number | null } = kept?.next ?? {}
101
111
  for (let queries = 0; queries < MERGED_QUERIES; queries++) {
102
112
  const page = (await githubGraphql(MERGED_QUERY, { owner, name, after }, token, fetchImpl) as MergedPage | null)?.repository?.pullRequests
103
113
  let done = page?.pageInfo?.hasNextPage !== true || typeof page.pageInfo.endCursor !== 'string'
@@ -106,6 +116,7 @@ export async function agentMerges(fullName: string, token: string, since: number
106
116
  if (updated === null) continue
107
117
  if (updated < stop) { done = true; break }
108
118
  from ??= updated
119
+ to = updated
109
120
  read++
110
121
  const [merged, merge] = [timeOf(node?.mergedAt), node?.mergeCommit?.oid]
111
122
  if (!node || merged === null || merged < since || typeof merge !== 'string' || !SHA.test(merge)) continue
@@ -129,10 +140,15 @@ export async function agentMerges(fullName: string, token: string, since: number
129
140
  for (const oid of brought) merges.set(oid, merged)
130
141
  if (subjects) rebases.set(merge, [merged, subjects, more])
131
142
  }
132
- if (done) return result(from ?? stop, null)
143
+ // Read as far back as it reads to: the year's start, every one since
144
+ // read; else as far as those read before reached.
145
+ if (done) return result(from ?? stop, null, stop > since ? kept?.reach : undefined)
133
146
  after = page!.pageInfo!.endCursor as string
147
+ // Read as far back as `need`: finished, so far back, those updated
148
+ // since read by the next as ever.
149
+ if (need > since && to !== null && to < need) return result(Math.max(from ?? stop, kept?.through ?? Number.NEGATIVE_INFINITY), null, to)
134
150
  }
135
- return result(kept?.through ?? null, { after: after!, from: from ?? stop, stop, read })
151
+ return result(kept?.through ?? null, { after: after!, from: from ?? stop, stop, read, ...to === null ? {} : { to } }, kept?.reach)
136
152
  }
137
153
 
138
154
  // Whether two reads of the pull requests merged (agentMerges) bring the
@@ -152,7 +168,7 @@ const headed = (subject: string, headline: string) => headline.endsWith('…') ?
152
168
  // (`read`, agentMerges): those it names, and of one rebased, its commits'
153
169
  // copies, the last and those below it on their first parents, as long as
154
170
  // each one's subject is that of one of its commits older than the last
155
- // matched, as Commits tells a rebase (repository-history.ts squashedPulls),
171
+ // matched, as Commits tells a rebase (repository-squashes.ts squashedPulls),
156
172
  // past those of its commits the rebase left none of (an empty one GitHub
157
173
  // counted no files of), and, past them all, as many more as it had before
158
174
  // its latest read; a squash merge's one commit alone, its subject the pull
@@ -6,14 +6,12 @@ import { GITHUB_AGENTS, type GithubAgent } from '../common/managed/github-agents
6
6
 
7
7
  // The agents known by their GitHub Apps or accounts, beside Claude and
8
8
  // Codex: those of GITHUB_AGENTS with branches of their own, in its order
9
- // (common/managed/github-agents.ts, where each is told), each its id and
10
- // name, its avatar (its mark, served by /api/avatar/<avatar>, a GitHub
11
- // App's, `app-<id>`), the fingerprint it leaves, the account its work is
12
- // authored by (its id and the logins GitHub names it by, told by both, or by
13
- // its id alone where it names none), and its branches' prefix.
14
- export const APP_AGENTS: readonly (AppAgent & AppAgentMarks)[] = (Object.entries(GITHUB_AGENTS) as [string, GithubAgent][])
15
- .flatMap(([id, { name, avatar, accounts: [account], prefix, fingerprint }]) => prefix ? [{ id, name, prefix, ...avatar ? { avatar } : {},
16
- ...account?.id === undefined ? {} : { account: { id: account.id, logins: account.logins } }, ...fingerprint ? { fingerprint } : {} }] : [])
9
+ // (common/managed/github-agents.ts, where each is told, its accounts among
10
+ // it), each its id and name, its avatar (its mark, served by
11
+ // /api/avatar/<avatar>, a GitHub App's, `app-<id>`), the fingerprint it
12
+ // leaves, and its branches' prefix.
13
+ export const APP_AGENTS: readonly (AppAgent & AppAgentMarks)[] = Object.entries<GithubAgent>(GITHUB_AGENTS)
14
+ .flatMap(([id, { name, avatar, prefix, fingerprint }]) => prefix ? [{ id, name, prefix, ...avatar ? { avatar } : {}, ...fingerprint ? { fingerprint } : {} }] : [])
17
15
  // The prefixes of the branches agents work on: Claude Code's and Codex's,
18
16
  // and the app agents'.
19
17
  export const AGENT_BRANCH_PREFIXES = ['claude/', 'codex/', ...APP_AGENTS.map(agent => agent.prefix)]
@@ -5,12 +5,13 @@ export class CacheMissError extends Error {
5
5
  constructor() { super('Managed cache unavailable') }
6
6
  }
7
7
 
8
- // Keys are internal relative paths; delete removes a whole directory of variants.
8
+ // Keys are internal relative paths; delete removes a whole directory of
9
+ // variants, but for one directory within it where `keep` names one.
9
10
  export interface CacheStorage {
10
11
  exists(key: string): Promise<boolean>
11
12
  put(key: string, bytes: Buffer): Promise<void>
12
13
  open(key: string): Promise<OpenedBlob>
13
- delete(prefix: string): Promise<void>
14
+ delete(prefix: string, keep?: string): Promise<void>
14
15
  }
15
16
 
16
17
  export function validateCacheKey(key: string): string {
@@ -10,7 +10,7 @@ export const MAX_COAUTHORS = 3
10
10
  // The rules co-authors are read by, the agents' addresses and names among
11
11
  // them: what was kept read by others (history pages, squashed pull requests)
12
12
  // read again by these, once raised.
13
- export const COAUTHORS_READ = 3
13
+ export const COAUTHORS_READ = 4
14
14
  const MAX_NAME = 256
15
15
  const textOf = (value: unknown, max: number) => typeof value === 'string' && value.trim() ? value.trim().slice(0, max) : null
16
16
 
@@ -30,6 +30,11 @@ const FINGERPRINTED = APP_AGENTS.filter(agent => agent.fingerprint)
30
30
  // commit gives (where it is read), and account.
31
31
  export type Maker = { name: unknown; email?: unknown; account: NamedAccount }
32
32
 
33
+ // Those a commit's message names in its `Co-authored-by:` trailers
34
+ // (agent-tasks.ts trailerBlock), each by name and address.
35
+ export const coauthorTrailers = (message: unknown) => typeof message === 'string'
36
+ ? [...trailerBlock(message.replaceAll('\r\n', '\n')).matchAll(COAUTHOR_TRAILER)].map(([, name, email]) => ({ name: name!, email: email! })) : []
37
+
33
38
  // The agent one who made a commit is: by their account, else the address
34
39
  // their commit names them by; else, GitHub naming no account for them, by
35
40
  // their name.
@@ -50,7 +55,7 @@ const makerAgent = ({ name, email, account }: Maker): GithubAgentId | null => gi
50
55
  // two people of one name with accounts or addresses of their own two.
51
56
  export function coauthorsOf(message: unknown, author: Maker, listed: Maker[] | null = null): Coauthor[] {
52
57
  const text = typeof message === 'string' ? message.replaceAll('\r\n', '\n') : ''
53
- const named = listed ?? [...trailerBlock(text).matchAll(COAUTHOR_TRAILER)].map(([, name, email]) => ({ name, email, account: commitAccount({ email }) }))
58
+ const named = listed ?? coauthorTrailers(text).map(({ name, email }) => ({ name, email, account: commitAccount({ email }) }))
54
59
  const signals = agentSignals([text], FINGERPRINTED)
55
60
  const marked = [...signals.claude || signals.sessions.length > 0 ? ['claude'] : [], ...signals.tasks.length > 0 || signals.codex ? ['codex'] : [],
56
61
  ...CURSOR_AGENT.test(text) ? ['cursor'] : [], ...signals.agents]
@@ -1,51 +1,52 @@
1
1
  // The versions a repository's lockfiles of npm's ecosystem lock at a commit,
2
2
  // as their review reads them (pull-dependency-reviews.ts): every lockfile
3
3
  // there (findRepoLockfiles) a review holds to (`held`, lockfile-scope.ts),
4
- // read as a pull request's are (pull-dependencies.ts readLockfile, kept in
5
- // this instance by the commit and path), as many as MAX_LOCKFILES of those,
6
- // each's versions (`lockfiles`), and those its reader refuses, of Bun's, or
7
- // past MAX_LOCKFILES (`unread`), as many read at once as READ_AT_ONCE; and
8
- // those of some of them (lockedIn), each version once by what its reviews
9
- // are held to (lockedWith), dev only where it is wherever it is locked.
10
- // GitHub having none of a lockfile it lists there fails the whole, as it
11
- // would for any file.
4
+ // read as a pull request's are (pull-dependencies.ts readLockfile), but kept
5
+ // by its blob, in this instance and beyond it (`kept`), as many as
6
+ // MAX_LOCKFILES of those, each's versions (`lockfiles`), and those its reader
7
+ // refuses, of Bun's, or past MAX_LOCKFILES (`unread`), as many read at once
8
+ // as READ_AT_ONCE; and those of some of them (lockedIn), each version once by
9
+ // what its reviews are held to (lockedWith), dev only where it is wherever it
10
+ // is locked. GitHub having none of a lockfile it lists there fails the
11
+ // whole, as it would for any file.
12
12
  import { createClient } from '@preventive/upstream/github.js'
13
13
  import { upstreamGithub } from './github-errors.ts'
14
14
  import { GithubApiError } from './github-app.ts'
15
15
  import type { LockedPackage } from './locked-packages.ts'
16
16
  import { type Lru, lru } from './lru.ts'
17
- import { eachOf, lockedWith, lockfileFormat, lockfileHeld, readLockfile } from './pull-dependencies.ts'
17
+ import { type GithubAsked, type ListedLockfile, type LockfileStore, eachOf, lockedWith, lockfileFormat, readLockfile } from './pull-dependencies.ts'
18
18
 
19
19
  export const MAX_LOCKFILES = 100
20
20
  const READ_AT_ONCE = 4
21
21
 
22
22
  export interface CommitLockfiles { lockfiles: { path: string; packages: LockedPackage[] }[]; unread: string[] }
23
- // Set where GitHub was asked for what a read reads (`github`), not all of it
24
- // held in this instance.
25
- export interface GithubAsked { github: boolean }
26
23
 
27
24
  // The lockfiles at commit `sha` its review reads, by their paths, each with
28
- // its format (pull-dependencies.ts lockfileFormat), kept by the commit;
29
- // `asked` told where GitHub was.
30
- const listings: Lru<{ path: string; format: NonNullable<ReturnType<typeof lockfileFormat>> }[]> = lru(20_000, listed => listed.length + 1)
31
- export async function lockfilesAt(fullName: string, token: string, sha: string, asked?: GithubAsked) {
25
+ // its format (pull-dependencies.ts lockfileFormat) and its blob's id, kept by
26
+ // the commit, in this instance and beyond it (`kept`); `asked` told where
27
+ // GitHub was.
28
+ const listings: Lru<ListedLockfile[]> = lru(20_000, listed => listed.length + 1)
29
+ export async function lockfilesAt(fullName: string, token: string, sha: string, kept: LockfileStore | null, asked?: GithubAsked) {
32
30
  const key = `${fullName}\0${sha}`
33
31
  const known = listings.get(key)
34
32
  if (known) return known
33
+ const stored = await kept?.listings.read(sha)
34
+ if (stored) { listings.set(key, stored); return stored }
35
35
  if (asked) asked.github = true
36
36
  const { lockfiles } = await upstreamGithub(() => createClient({ token, userAgent: 'deepview-triage' }).findRepoLockfiles({ repo: fullName, sha }))
37
- const listed = lockfiles.flatMap(({ path }) => { const format = lockfileFormat(path); return format ? [{ path, format }] : [] })
37
+ const listed = lockfiles.flatMap(({ path, sha: blob }) => { const format = lockfileFormat(path); return format ? [{ path, format, sha: blob }] : [] })
38
38
  listings.set(key, listed)
39
+ await kept?.listings.write(sha, listed)
39
40
  return listed
40
41
  }
41
42
 
42
- export async function lockedAt(fullName: string, token: string, sha: string, held: (path: string) => boolean = () => true, asked?: GithubAsked): Promise<CommitLockfiles> {
43
- const listed = (await lockfilesAt(fullName, token, sha, asked)).filter(({ path }) => held(path))
43
+ export async function lockedAt(fullName: string, token: string, sha: string, kept: LockfileStore | null, held: (path: string) => boolean = () => true,
44
+ asked?: GithubAsked): Promise<CommitLockfiles> {
45
+ const listed = (await lockfilesAt(fullName, token, sha, kept, asked)).filter(({ path }) => held(path))
44
46
  const unread = listed.slice(MAX_LOCKFILES).map(({ path }) => path)
45
47
  const lockfiles: CommitLockfiles['lockfiles'] = []
46
48
  const reading = listed.slice(0, MAX_LOCKFILES)
47
- if (asked && reading.some(lockfile => !lockfileHeld(fullName, sha, lockfile))) asked.github = true
48
- const reads = await eachOf(reading, READ_AT_ONCE, lockfile => readLockfile(fullName, token, sha, lockfile))
49
+ const reads = await eachOf(reading, READ_AT_ONCE, lockfile => readLockfile(fullName, token, sha, lockfile, kept, asked))
49
50
  for (const [at, lockfile] of reading.entries()) {
50
51
  const read = reads[at]!
51
52
  if ('error' in read && read.error === 'github-not-found') throw new GithubApiError(404, 'github-not-found')
@@ -494,7 +494,7 @@ export interface ManagedDb extends DependencyReviewStore, RepositoryDependencyRe
494
494
  getBundleByIntegrity(integrity: string): Promise<ManagedBundle | null>
495
495
  resolveBundleUpload(integrity: string): Promise<ManagedBundle | null>
496
496
  getBundle(id: string): Promise<ManagedBundle | null>
497
- // One read snapshot of the live session, bundle and grants. Passing a team
497
+ // One statement reads the live session, bundle and grants. Passing a team
498
498
  // (or null for any team) also checks advisory security access; omission skips it.
499
499
  getBundleAccessSnapshot(sessionId: string, now: number, id: string, advisoriesTeamId?: string | null): Promise<{
500
500
  bundle: ManagedBundle | null; canReadAdvisories: boolean
@@ -697,13 +697,6 @@ function prepareStatements(db: ManagedSql) {
697
697
  insertSessionStmt: db.prepare(
698
698
  `INSERT INTO managed_session (id, user_id, csrf_token, created_at, expires_at) VALUES (?, ?, ?, ?, ?)`,
699
699
  ),
700
- selectSessionStmt: db.prepare(
701
- `SELECT s.id AS id, s.csrf_token AS csrf, s.expires_at AS exp, s.upload_key AS uploadKey,
702
- u.id AS uid, u.login AS login, u.name AS name, u.avatar_url AS avatar, u.role AS role
703
- FROM managed_session s
704
- JOIN managed_user u ON u.id = s.user_id
705
- WHERE s.id = ? AND s.expires_at > ?`,
706
- ),
707
700
  selectOwnSessionStmt: db.prepare(
708
701
  `SELECT s.id AS id, s.csrf_token AS csrf, s.expires_at AS exp, s.upload_key AS uploadKey,
709
702
  u.id AS uid, u.login AS login, u.name AS name, u.avatar_url AS avatar, u.role AS role
@@ -994,15 +987,18 @@ function prepareStatements(db: ManagedSql) {
994
987
  ),
995
988
  selectTriageFixesStmt: db.prepare(`SELECT DISTINCT fix FROM managed_finding_triage
996
989
  WHERE finding_id IN (SELECT value FROM json_each(?)) AND fix IS NOT NULL ORDER BY fix`),
997
- annotationTriageRevisionStmt: db.prepare(`
998
- SELECT t.finding_id AS id,
999
- (SELECT e.batch_id FROM managed_finding_triage_event e
1000
- WHERE e.finding_id = t.finding_id ORDER BY e.seq DESC LIMIT 1) AS revision
1001
- FROM managed_finding_triage t WHERE t.finding_id IN (SELECT value FROM json_each(?))
1002
- ORDER BY t.finding_id`),
1003
- annotationCommentRevisionStmt: db.prepare(`
1004
- SELECT id, version FROM managed_finding_comment
1005
- WHERE finding_id IN (SELECT value FROM json_each(?)) ORDER BY id`),
990
+ // Each finding's latest triage batch, comment versions and linked issues,
991
+ // in one statement.
992
+ annotationRevisionStmt: db.prepare(
993
+ `WITH requested AS (SELECT value AS id FROM json_each(?))
994
+ SELECT (SELECT json_group_array(json_object('id', t.finding_id, 'revision', (SELECT e.batch_id FROM managed_finding_triage_event e
995
+ WHERE e.finding_id = t.finding_id ORDER BY e.seq DESC LIMIT 1)) ORDER BY t.finding_id)
996
+ FROM managed_finding_triage t WHERE t.finding_id IN (SELECT id FROM requested)) AS triage,
997
+ (SELECT json_group_array(json_object('id', c.id, 'version', c.version) ORDER BY c.id)
998
+ FROM managed_finding_comment c WHERE c.finding_id IN (SELECT id FROM requested)) AS comments,
999
+ (SELECT json_group_array(json_object('findingId', i.finding_id, 'issueUrl', i.issue_url, 'autoFixUrl', i.auto_fix_url) ORDER BY i.finding_id)
1000
+ FROM managed_finding_issue i WHERE i.finding_id IN (SELECT id FROM requested) AND i.issue_url IS NOT NULL) AS issues`,
1001
+ ),
1006
1002
  insertBundleStmt: db.prepare(
1007
1003
  `WITH candidate(id, slug) AS (VALUES (?, ?))
1008
1004
  INSERT INTO managed_bundle (id, slug, integrity, filename, kind, byte_size, uploaded_by, uploaded_by_login, repo_id, repo_directory, visible, uploaded_at, data_key, storage_encrypted, provenance, build_conditions)
@@ -1157,6 +1153,32 @@ function prepareStatements(db: ManagedSql) {
1157
1153
  WHERE b.id = ? AND tu.user_id = ? AND tu.view_security = 1
1158
1154
  AND (? IS NULL OR tr.team_id = ?) LIMIT 1`,
1159
1155
  ),
1156
+ // A bundle access snapshot's reads in one statement, so one snapshot of
1157
+ // the database answers them all: the session's user, view sessions
1158
+ // included; the bundle, NULL where there is none; whether one of the
1159
+ // user's team scopes contains it, the grant both selectBundleReadableStmt
1160
+ // (a manager's, beside their own uploads) and selectRepoPathReadableStmt
1161
+ // (a reader's, of its repository directory) ask for; and, where the flag
1162
+ // asks, whether one in the team (any, where NULL) shows them its
1163
+ // advisories (selectBundleSecurityStmt).
1164
+ selectBundleAccessStmt: db.prepare(
1165
+ `WITH viewer AS (
1166
+ SELECT u.id AS uid, u.role AS role
1167
+ FROM managed_session s JOIN managed_user u ON u.id = s.user_id
1168
+ WHERE s.id = ? AND s.expires_at > ?
1169
+ )
1170
+ SELECT v.uid AS uid, v.role AS role, b.id AS id, b.slug AS slug, b.integrity AS integrity, b.filename AS filename, b.kind AS kind,
1171
+ b.byte_size AS byteSize, b.uploaded_by AS uploadedBy, b.repo_id AS repoId, b.repo_directory AS repoDirectory, b.visible AS visible,
1172
+ b.uploaded_at AS uploadedAt, b.provenance AS provenance, b.build_conditions AS buildConditions,
1173
+ EXISTS(SELECT 1 FROM managed_team_repo tr JOIN managed_team_user tu ON tu.team_id = tr.team_id
1174
+ JOIN managed_team t ON t.id = tu.team_id AND t.hidden = 0
1175
+ WHERE tr.repo_id = b.repo_id AND ${BUNDLE_IN_TEAM_PATH_SQL} AND tu.user_id = v.uid) AS granted,
1176
+ (? = 1 AND EXISTS(SELECT 1 FROM managed_team_repo tr JOIN managed_team_user tu ON tu.team_id = tr.team_id
1177
+ JOIN managed_team t ON t.id = tu.team_id AND t.hidden = 0
1178
+ WHERE tr.repo_id = b.repo_id AND ${BUNDLE_IN_TEAM_PATH_SQL} AND tu.user_id = v.uid AND tu.view_security = 1
1179
+ AND (? IS NULL OR tr.team_id = ?))) AS security
1180
+ FROM viewer v LEFT JOIN managed_bundle b ON b.id = ?`,
1181
+ ),
1160
1182
  // The viewer's effective visibility permissions for a report: OR'd (MAX over
1161
1183
  // 0/1) across memberships whose repository path contains the report. NULLs
1162
1184
  // (no such membership) read as 0 = no permission.
@@ -1439,15 +1461,10 @@ function triageMethods(stmts: ReturnType<typeof prepareStatements>, historyLimit
1439
1461
  return {
1440
1462
  async getAnnotationRevision(findingIds: readonly string[]): Promise<string> {
1441
1463
  if (findingIds.length === 0) return '[]'
1442
- const ids = JSON.stringify(findingIds)
1443
1464
  // Batch UUIDs survive history trimming and distinguish same-timestamp
1444
1465
  // writes, even if SQLite reuses a sequence after an annotation purge.
1445
- return JSON.stringify([
1446
- await stmts.annotationTriageRevisionStmt.all(ids),
1447
- await stmts.annotationCommentRevisionStmt.all(ids),
1448
- await db.prepare(`SELECT finding_id, issue_url, auto_fix_url FROM managed_finding_issue
1449
- WHERE finding_id IN (SELECT value FROM json_each(?)) AND issue_url IS NOT NULL ORDER BY finding_id`).all(ids),
1450
- ])
1466
+ const row = await stmts.annotationRevisionStmt.get(JSON.stringify(findingIds)) as { triage: unknown; comments: unknown; issues: unknown }
1467
+ return JSON.stringify([jsonRows(row.triage), jsonRows(row.comments), jsonRows(row.issues)])
1451
1468
  },
1452
1469
  async deleteTriage(findingIds: readonly string[]): Promise<number> {
1453
1470
  if (findingIds.length === 0) return 0
@@ -1579,22 +1596,19 @@ function bundleMethods(stmts: ReturnType<typeof prepareStatements>, db: ManagedS
1579
1596
  return row == null ? null : mapBundle(row)
1580
1597
  },
1581
1598
  async getBundleAccessSnapshot(sessionId: string, now: number, id: string, advisoriesTeamId?: string | null) {
1582
- const session = await stmts.selectSessionStmt.get(sessionId, now) as SessionRow | undefined
1583
- if (!session) return null
1599
+ // One statement, shared by SQLite and Postgres; the advisories grant is
1600
+ // read only where a team (or null, any) is passed.
1601
+ const team = advisoriesTeamId ?? null
1602
+ const row = await stmts.selectBundleAccessStmt.get(sessionId, now, advisoriesTeamId === undefined ? 0 : 1, team, team, id) as
1603
+ { uid: string; role: Role; granted: unknown; security: unknown } & (BundleRow | { id: null }) | undefined
1604
+ if (!row) return null
1584
1605
  const denied = { bundle: null, canReadAdvisories: false }
1585
- if (!roleAtLeast(session.role, 'view')) return denied
1586
- const row = await selectBundleStmt.get(id) as BundleRow | undefined
1587
- if (!row) return denied
1588
- const manager = roleAtLeast(session.role, 'manage')
1606
+ if (!roleAtLeast(row.role, 'view') || row.id === null) return denied
1607
+ const manager = roleAtLeast(row.role, 'manage')
1589
1608
  if (!manager && row.visible !== 1) return denied
1590
- if (session.role !== 'admin') {
1591
- const allowed = manager ? await selectBundleReadableStmt.get(id, session.uid, session.uid)
1592
- : row.repoId !== null && await selectRepoPathReadableStmt.get(row.repoDirectory, row.repoId, session.uid)
1593
- if (!allowed) return denied
1594
- }
1595
- const canReadAdvisories = manager || (advisoriesTeamId !== undefined
1596
- && await stmts.selectBundleSecurityStmt.get(id, session.uid, advisoriesTeamId, advisoriesTeamId) != null)
1597
- return { bundle: mapBundle(row), canReadAdvisories }
1609
+ // Admins read every bundle, managers their own uploads too.
1610
+ if (row.role !== 'admin' && !(row.granted || (manager && row.uploadedBy === row.uid))) return denied
1611
+ return { bundle: mapBundle(row), canReadAdvisories: manager || Boolean(row.security) }
1598
1612
  },
1599
1613
  async listBundles(userId?: string): Promise<AdminBundle[]> {
1600
1614
  const rows = (await selectBundlesStmt.all(userId ?? null, userId ?? null, userId ?? null)) as BundleListRow[]
@@ -1,4 +1,4 @@
1
- import { DEPENDENCY_REVIEW_SCHEMA } from './dependency-reviews.ts'
1
+ import { DEPENDENCY_REVIEW_COLUMNS, DEPENDENCY_REVIEW_SCHEMA } from './dependency-reviews.ts'
2
2
  import { LINK_REPORT_SCHEMA } from './link-reports.ts'
3
3
  import { REPOSITORY_ALIAS_SCHEMA } from './repository-aliases.ts'
4
4
  import { TEAM_NPM_SCOPE_SCHEMA } from './team-npm-scopes.ts'
@@ -17,6 +17,7 @@ import { UPSTREAM_CACHE_SCHEMA } from './upstream-cache.ts'
17
17
  import { COMMENT_SCHEMA } from './comments.ts'
18
18
  import { ACTIVITY_SCHEMA, uploadAction } from './activity.ts'
19
19
  import { type ManagedSqlDriver, scopeManagedMethods } from './sql.ts'
20
+ import { countStatement, timed } from './request-timing.ts'
20
21
  import { postgresSchema, postgresSql } from './sql-postgres.ts'
21
22
  import { managedTableRenames } from './db-table-names.ts'
22
23
  import { WORKSPACE_SHARE_SCHEMA } from './workspace-shares.ts'
@@ -38,7 +39,7 @@ const LOCK = 'SELECT pg_advisory_xact_lock(1937006964, 1835101793)'
38
39
  // Whether every version up to the latest is recorded, in one round trip at
39
40
  // a cold start: a database with no versions table yet (undefined_table) has
40
41
  // none. Outside a transaction, so the failed read aborts nothing.
41
- const SCHEMA_VERSIONS = 31
42
+ const SCHEMA_VERSIONS = 32
42
43
  async function currentSchema(db: PgConnection): Promise<boolean> {
43
44
  try {
44
45
  const { rows } = await db.query(`SELECT count(*) AS n FROM managed_schema_version WHERE version BETWEEN 1 AND ${SCHEMA_VERSIONS}`)
@@ -183,6 +184,14 @@ async function migrateRepoUploadIndexes(db: PgConnection): Promise<void> {
183
184
  await db.query('INSERT INTO managed_schema_version VALUES (31)')
184
185
  }
185
186
 
187
+ // The shasums of the tarballs a dependency review reads, none known of
188
+ // those written before.
189
+ async function migrateDependencyReviewShasums(db: PgConnection): Promise<void> {
190
+ if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 32')).rows.length > 0) return
191
+ await db.query(`ALTER TABLE managed_dependency_review ${DEPENDENCY_REVIEW_COLUMNS.map(([column, type]) => `ADD COLUMN IF NOT EXISTS ${column} ${type}`).join(', ')}`)
192
+ await db.query('INSERT INTO managed_schema_version VALUES (32)')
193
+ }
194
+
186
195
  async function initialize(db: PgConnection): Promise<void> {
187
196
  if (await currentSchema(db)) return
188
197
  await db.query('BEGIN')
@@ -257,7 +266,7 @@ async function initialize(db: PgConnection): Promise<void> {
257
266
  await db.query(postgresSchema(revisionSchema(true)) + '; INSERT INTO managed_schema_version VALUES (11)')
258
267
  }
259
268
  for (const migrate of [migrateRepositoryDefaultCache, migrateBundleBuildLeases, migrateBundleVisibility, migrateLinkReports, migrateRepositoryAliases, migrateHiddenTeams, migrateIssueFixes, migrateBundleProvenance, migrateUpstreamCache, migrateBundleBuildConditions, migrateSessionUploadKeys, migrateViewSessions, migrateGithubCommits, migrateTeamNpmScopes, migrateDependencyReviews, migrateGithubLinks, migrateRepositoryDependencyReviews,
260
- migrateRepositoryDependencyReviewLockfiles, migrateRepoUploadIndexes]) await migrate(db)
269
+ migrateRepositoryDependencyReviewLockfiles, migrateRepoUploadIndexes, migrateDependencyReviewShasums]) await migrate(db)
261
270
  await db.query('COMMIT')
262
271
  } catch (err) {
263
272
  await db.query('ROLLBACK')
@@ -325,7 +334,12 @@ function requestConnections(connect: PgConnect) {
325
334
  }
326
335
 
327
336
  // Exposed for parity tests against real PostgreSQL semantics via PGlite.
328
- export async function openPostgresManagedDb(connect: PgConnect, options: ManagedDbOptions = {}, reuseConnections = false): Promise<ManagedDb> {
337
+ export async function openPostgresManagedDb(leased: PgConnect, options: ManagedDbOptions = {}, reuseConnections = false): Promise<ManagedDb> {
338
+ // Each statement told in its request's Server-Timing (request-timing.ts).
339
+ const connect: PgConnect = async () => {
340
+ const connection = await leased()
341
+ return { query: (sql, params) => { countStatement(); return connection.query(sql, params) }, release: discard => connection.release(discard) }
342
+ }
329
343
  const initial = await connect()
330
344
  try { await initialize(initial) } catch (err) { await initial.release(true).catch(() => {}); throw err }
331
345
  await initial.release()
@@ -431,7 +445,9 @@ export async function openNeonManagedDb(url: string, options: ManagedDbOptions =
431
445
  let db: ManagedDb
432
446
  try {
433
447
  db = await openPostgresManagedDb(async () => {
434
- const client = await pool.connect()
448
+ // A lease's wait told in its request's Server-Timing: a new connection's
449
+ // TLS, WebSocket and Postgres handshake, where the pool had none idle.
450
+ const client = await timed('conn', () => pool.connect())
435
451
  if (!watched.has(client)) { watched.add(client); client.on('error', () => { failed.add(client) }) }
436
452
  return { query: (sql, params) => client.query(sql, params),
437
453
  release: discard => Promise.try(() => client.release(discard === true || failed.has(client))) }
@@ -10,6 +10,7 @@ import { revisionSchema } from './revisions.ts'
10
10
  import { MANAGED_SCHEMA, REPO_UPLOAD_INDEXES, VIEW_SESSION_INDEX } from './db-schema.ts'
11
11
  import { GITHUB_LINK_USER_COLUMNS } from './github-links.ts'
12
12
  import { REPOSITORY_DEPENDENCY_REVIEW_COLUMNS } from './repository-dependency-reviews.ts'
13
+ import { DEPENDENCY_REVIEW_COLUMNS } from './dependency-reviews.ts'
13
14
  import { STORAGE_SCHEMA } from './storage-db.ts'
14
15
  import { GITHUB_COMMIT_SCHEMA, GITHUB_METADATA_SCHEMA, GITHUB_STATE_REASON_COLUMN } from './github-metadata.ts'
15
16
  import { MANAGED_ISSUE_SCHEMA } from './managed-issues.ts'
@@ -124,6 +125,7 @@ export function openSqliteManagedDb(path: string, options: ManagedDbOptions = {}
124
125
  ensureColumn(db, 'managed_finding_issue', 'auto_fix_checked_at', 'INTEGER')
125
126
  ensureColumn(db, 'managed_selected_repo', 'active', 'INTEGER NOT NULL DEFAULT 1')
126
127
  for (const [column, type] of REPOSITORY_DEPENDENCY_REVIEW_COLUMNS) ensureColumn(db, 'managed_selected_repo', column, type)
128
+ for (const [column, type] of DEPENDENCY_REVIEW_COLUMNS) ensureColumn(db, 'managed_dependency_review', column, type)
127
129
  ensureColumn(db, 'managed_finding_triage_event', 'report_id', 'TEXT')
128
130
  ensureColumn(db, 'managed_finding_triage_event', 'report', 'TEXT')
129
131
  ensureColumn(db, 'managed_finding_triage_event', 'repo', 'TEXT')
@@ -1,6 +1,6 @@
1
1
  // A repository's Dependabot alerts for a repository risk scan, read with the
2
2
  // App's installation token where the admin's own access lets them see them on
3
- // GitHub, as the README's repository risk section tells.
3
+ // GitHub, as doc/bundles.md's repository risk scan section tells.
4
4
  import type { ManagedConfig } from './config.ts'
5
5
  import type { SelectedRepo } from './db.ts'
6
6
  import { type DependabotAlert, dependabotAlerts } from './dependabot-alerts.ts'
@@ -18,11 +18,14 @@ const MAX_FORMATTED = 1_000_000
18
18
  // read after, every one formatted.
19
19
  const REVIEWS_AT_ONCE = 200
20
20
 
21
- // `review` as the hook is given it, the version it is of as `locked`, a
22
- // pull request's lockfile, locks it where it is one's.
21
+ // `review` as the hook is given it: each version with the integrity and
22
+ // shasum the review keeps of the tarball it read, and the version it is of
23
+ // with the patch `locked`, a pull request's lockfile, locks it with, where it
24
+ // is one's.
23
25
  function entryOf(review: DependencyReview, locked?: AddedPackage | undefined): DependencyReviewEntry {
24
- return { ecosystem: 'npm', old: review.basePackage === null ? null : { name: review.basePackage, version: review.baseVersion ?? '', integrity: review.baseIntegrity ?? '' },
25
- new: { name: review.package, version: review.version, integrity: review.integrity, ...locked?.shasum ? { shasum: locked.shasum } : {},
26
+ return { ecosystem: 'npm', old: review.basePackage === null ? null : { name: review.basePackage, version: review.baseVersion ?? '', integrity: review.baseIntegrity ?? '',
27
+ ...review.baseShasum ? { shasum: review.baseShasum } : {} },
28
+ new: { name: review.package, version: review.version, integrity: review.integrity, ...review.shasum ? { shasum: review.shasum } : {},
26
29
  ...locked?.patchHash ? { patchHash: locked.patchHash } : {} },
27
30
  status: review.status!, coverage: review.coverage === 'full' ? 'full' : 'partial', comment: review.comment,
28
31
  files: review.entries.filter(entry => entry.status !== null || entry.comment.trim() !== '').map(entry => ({ path: entry.file, status: entry.status, comment: entry.comment })),