@preventive/triage 1.0.0-alpha.44 → 1.0.0-alpha.45

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 (183) hide show
  1. package/common/bundle-sources.js +20 -1
  2. package/common/default-scan-models.ts +1 -1
  3. package/common/dependency-reviews.d.ts +0 -5
  4. package/common/dependency-reviews.js +0 -17
  5. package/common/managed/agent-tasks.ts +292 -0
  6. package/common/managed/github-agents.ts +92 -0
  7. package/common/managed/github-logins.ts +49 -0
  8. package/common/managed/roles.ts +7 -0
  9. package/common/managed/routes.js +88 -7
  10. package/common/managed/team-app.d.ts +5 -2
  11. package/common/managed/team-app.js +2 -2
  12. package/common/server-info.ts +3 -0
  13. package/common/workspace-app.js +6 -3
  14. package/out/brotli-fallback.js +3 -3
  15. package/out/chunk-2XUEZHIP.js +18 -18
  16. package/out/chunk-2ZDZWLFY.js +1 -0
  17. package/out/chunk-3PS4KWD2.js +10 -0
  18. package/out/chunk-5DHL5NG3.js +2 -0
  19. package/out/chunk-6HNXNIQY.js +52 -0
  20. package/out/chunk-7C7DWL7I.js +1 -0
  21. package/out/chunk-AFLZH7WG.js +1 -0
  22. package/out/chunk-BC2UCLFJ.js +1 -0
  23. package/out/chunk-C4Z6ZBXV.js +6 -6
  24. package/out/chunk-GDPCLSOS.js +2 -0
  25. package/out/chunk-GO7FT4OD.js +1 -0
  26. package/out/chunk-HMAJDVXN.js +1 -0
  27. package/out/chunk-I2MDGSQX.js +3 -0
  28. package/out/chunk-IYPY47MC.js +1 -0
  29. package/out/chunk-IZ7POWI3.js +18 -0
  30. package/out/chunk-JCIIBLKR.js +1 -1
  31. package/out/chunk-NPW52BM2.js +17 -0
  32. package/out/chunk-NRPC74AM.js +1 -1
  33. package/out/chunk-NV7ES4EW.js +1 -0
  34. package/out/chunk-OKRLLE5I.js +17 -0
  35. package/out/chunk-OOJM4CTU.js +1 -1
  36. package/out/chunk-PNHBZGDD.js +1 -1
  37. package/out/chunk-PTGDYLNS.js +36 -0
  38. package/out/chunk-PXK7EU4V.js +1 -0
  39. package/out/chunk-Q6MVYGYV.js +3 -0
  40. package/out/chunk-QB4CBOX2.js +1 -0
  41. package/out/chunk-QRD6GIG5.js +229 -0
  42. package/out/chunk-R3NZFXVR.js +1 -0
  43. package/out/chunk-R5O6FAW4.js +1 -1
  44. package/out/chunk-SXV2T5C4.js +80 -0
  45. package/out/chunk-TUII34K2.js +1 -1
  46. package/out/chunk-VFWMQ3LG.js +2 -0
  47. package/out/chunk-VH4XMZIW.js +2 -0
  48. package/out/chunk-WFYLMF6Z.js +10 -0
  49. package/out/chunk-WI7Z5A2T.js +6 -0
  50. package/out/chunk-WQ74ZBZF.js +7 -0
  51. package/out/chunk-XVBEOGOS.js +3 -0
  52. package/out/client-managed-import.js +3 -3
  53. package/out/client-managed.js +31 -35
  54. package/out/client-sync.js +11 -11
  55. package/out/compare.js +35 -0
  56. package/out/dialogs.js +51 -0
  57. package/out/github-issue-authorized.html +1 -1
  58. package/out/github-issue-authorized.js +1 -1
  59. package/out/graph.js +17 -17
  60. package/out/index.html +30 -34
  61. package/out/prism.js +14 -14
  62. package/out/scan.js +2 -0
  63. package/out/terminal.js +294 -293
  64. package/out/view.css +1 -1
  65. package/out/view.js +16 -107
  66. package/package.json +51 -6
  67. package/risk/actions/findings.js +156 -0
  68. package/risk/actions/resolve.js +219 -0
  69. package/risk/actions/scripts/commands.js +182 -0
  70. package/risk/actions/scripts/grade.js +113 -0
  71. package/risk/actions/scripts/packages.js +123 -0
  72. package/risk/actions/scripts/shell.js +165 -0
  73. package/risk/actions/uses.js +76 -0
  74. package/risk/actions/workflow.js +110 -0
  75. package/risk/files/hosts.js +6 -6
  76. package/risk/files/readability.js +23 -17
  77. package/risk/files/runs-code.js +18 -6
  78. package/risk/files/script.js +7 -3
  79. package/risk/files/stamps.js +8 -7
  80. package/risk/index.d.ts +68 -1
  81. package/risk/index.js +6 -2
  82. package/risk/npm/assess.js +1 -1
  83. package/risk/npm/compare.js +34 -10
  84. package/risk/npm/files.js +413 -2
  85. package/risk/npm/tarball.js +56 -73
  86. package/server-managed/agent-merges.ts +198 -0
  87. package/server-managed/app-agents.ts +30 -0
  88. package/server-managed/avatar-store.ts +1 -0
  89. package/server-managed/commit-coauthors.ts +83 -0
  90. package/server-managed/commit-lockfiles.ts +62 -0
  91. package/server-managed/db-methods.ts +29 -10
  92. package/server-managed/db-neon.ts +30 -2
  93. package/server-managed/db-schema.ts +9 -2
  94. package/server-managed/db.ts +7 -0
  95. package/server-managed/dependabot-access.ts +60 -0
  96. package/server-managed/dependabot-alerts.ts +185 -0
  97. package/server-managed/dependency-review-comments.ts +97 -0
  98. package/server-managed/dependency-reviews.ts +65 -15
  99. package/server-managed/github-accounts.ts +33 -0
  100. package/server-managed/github-actions.ts +71 -0
  101. package/server-managed/github-app.ts +23 -9
  102. package/server-managed/github-errors.ts +2 -0
  103. package/server-managed/github-link-oauth.ts +69 -0
  104. package/server-managed/github-links.ts +204 -0
  105. package/server-managed/github-oauth.ts +5 -2
  106. package/server-managed/github-users.ts +30 -0
  107. package/server-managed/http.ts +1122 -52
  108. package/server-managed/index.ts +6 -2
  109. package/server-managed/locked-packages.ts +37 -16
  110. package/server-managed/lockfile-scope.ts +49 -0
  111. package/server-managed/npm-delta-worker.js +23 -0
  112. package/server-managed/npm-delta.ts +173 -0
  113. package/server-managed/npm-insights.ts +20 -11
  114. package/server-managed/npm-kept.ts +51 -4
  115. package/server-managed/npm-loads.ts +20 -13
  116. package/server-managed/npm-packages.ts +24 -64
  117. package/server-managed/pull-comment-reviews.ts +161 -0
  118. package/server-managed/pull-dependencies.ts +280 -0
  119. package/server-managed/pull-dependency-reviews.ts +318 -0
  120. package/server-managed/pull-reviews.ts +205 -0
  121. package/server-managed/release-commits.ts +84 -0
  122. package/server-managed/repository-actions.ts +209 -0
  123. package/server-managed/repository-agents.ts +220 -0
  124. package/server-managed/repository-browser.ts +54 -13
  125. package/server-managed/repository-changes.ts +284 -0
  126. package/server-managed/repository-code.ts +52 -0
  127. package/server-managed/repository-dependency-reviews.ts +135 -0
  128. package/server-managed/repository-history.ts +445 -0
  129. package/server-managed/repository-issues.ts +181 -0
  130. package/server-managed/repository-pulse.ts +432 -0
  131. package/server-managed/repository-risk-read.ts +165 -0
  132. package/server-managed/repository-risk.ts +113 -105
  133. package/server-managed/reviewed-dependencies.ts +149 -0
  134. package/server-managed/session.ts +9 -5
  135. package/server-managed/sql.ts +1 -1
  136. package/server-managed/storage-stores.ts +5 -0
  137. package/server-managed/team-app.ts +19 -3
  138. package/server-managed/workspace-shares.ts +4 -1
  139. package/server.ts +11 -0
  140. package/out/chunk-2ABLKCOY.js +0 -46
  141. package/out/chunk-2KBF5TZ7.js +0 -2
  142. package/out/chunk-35VE6CDH.js +0 -2
  143. package/out/chunk-3BZ7JERB.js +0 -1
  144. package/out/chunk-4XCDFGDP.js +0 -10
  145. package/out/chunk-5LSXOLUS.js +0 -1
  146. package/out/chunk-5RCM2J63.js +0 -1
  147. package/out/chunk-7B6HB4D7.js +0 -1
  148. package/out/chunk-AJ4GTATZ.js +0 -3
  149. package/out/chunk-CH4UN6WK.js +0 -225
  150. package/out/chunk-CMQA4DM6.js +0 -1
  151. package/out/chunk-CVVJAXVY.js +0 -2
  152. package/out/chunk-DIJJNTRH.js +0 -10
  153. package/out/chunk-EDKLBQQN.js +0 -17
  154. package/out/chunk-F47DXCEV.js +0 -33
  155. package/out/chunk-FK3DZEKL.js +0 -1
  156. package/out/chunk-G6AJWENO.js +0 -1
  157. package/out/chunk-GJ4IUCNS.js +0 -1
  158. package/out/chunk-GNXOKIGP.js +0 -80
  159. package/out/chunk-GZ4MS3TE.js +0 -13
  160. package/out/chunk-GZWM7UQM.js +0 -2
  161. package/out/chunk-IU3CCINR.js +0 -3
  162. package/out/chunk-K3X5EJBQ.js +0 -2
  163. package/out/chunk-KEX2ATEW.js +0 -2
  164. package/out/chunk-KYCNJVB7.js +0 -1
  165. package/out/chunk-LPCTJYNA.js +0 -7
  166. package/out/chunk-O4RJY5TP.js +0 -2
  167. package/out/chunk-OD4LPN3H.js +0 -1
  168. package/out/chunk-QWDWTBM7.js +0 -1
  169. package/out/chunk-QYWKC557.js +0 -1
  170. package/out/chunk-R75XOKUP.js +0 -1
  171. package/out/chunk-SWTLADW5.js +0 -1
  172. package/out/chunk-T7OKH4VW.js +0 -1
  173. package/out/chunk-TZ5KB2Y5.js +0 -10
  174. package/out/chunk-UNTRGE26.js +0 -2
  175. package/out/chunk-UTR2LVME.js +0 -2
  176. package/out/chunk-UYAXVRVL.js +0 -1
  177. package/out/chunk-V3ME6FI5.js +0 -2
  178. package/out/chunk-V5O7M2YT.js +0 -1
  179. package/out/chunk-VIHGO77Y.js +0 -1
  180. package/out/chunk-W4J3MGLB.js +0 -1
  181. package/out/chunk-WGG7KQUH.js +0 -1
  182. package/out/chunk-XYFXXB2G.js +0 -10
  183. package/out/chunk-ZESLQCTF.js +0 -6
@@ -28,6 +28,13 @@ export const MANAGED_PAGES = Object.freeze({
28
28
  'manage-history': '/manage/history',
29
29
  })
30
30
 
31
+ // A team's lists, each at /team/<slug>/<what it lists>; its dependencies'
32
+ // sub-tabs past the first at /team/<slug>/dependencies/<tab>.
33
+ const WORKSPACE_CONTENT_VIEWS = new Set(['workspace-reports', 'workspace-bundles', 'workspace-repositories', 'workspace-dependencies'])
34
+ export const DEPENDENCY_TABS = new Set(['pulls', 'versions', 'branch', 'reviews'])
35
+ // A route's `dependencyTab` field, where it names one.
36
+ const dependencyTabField = route => route.view === 'workspace-dependencies' && route.dependencyTab != null ? { dependencyTab: route.dependencyTab } : {}
37
+
31
38
  // Dot-only path components are normalised away by URL parsers, even escaped.
32
39
  function findingPath(id) {
33
40
  if (!isLinkableFindingId(id) || id === '.' || id === '..') return null
@@ -38,9 +45,13 @@ export function managedRoutePath(route) {
38
45
  if (!route) return null
39
46
  if (route.view === 'home') return '/'
40
47
  if (route.view === 'reviews') return '/my/reviews'
48
+ if (route.view === 'profile') return '/my/profile'
41
49
  if (route.view === 'npm') return npmPackagePath(route)
42
- if (route.view === 'workspace-reports' || route.view === 'workspace-bundles') {
43
- return /^[A-Za-z0-9_-]+$/u.test(route.teamSlug ?? '') ? `/team/${route.teamSlug}/${route.view.slice(10)}` : null
50
+ if (route.view === 'repository-code') return repositoryCodePath(route)
51
+ if (WORKSPACE_CONTENT_VIEWS.has(route.view)) {
52
+ const tab = dependencyTabField(route).dependencyTab ?? 'pulls'
53
+ if (!/^[A-Za-z0-9_-]+$/u.test(route.teamSlug ?? '') || !DEPENDENCY_TABS.has(tab)) return null
54
+ return `/team/${route.teamSlug}/${route.view.slice(10)}${tab === 'pulls' ? '' : `/${tab}`}`
44
55
  }
45
56
  if (route.view === 'bundles') {
46
57
  if (!/^[A-Za-z0-9_-]+$/u.test(route.bundleSlug ?? '')
@@ -128,6 +139,66 @@ function parseNpmRoute(path, hash) {
128
139
  return !match[5] && /^[1-9]\d*$/u.test(match[4]) && Number.isSafeInteger(file) ? { ...route, file, ...codeLines(hash) } : null
129
140
  }
130
141
 
142
+ // The code viewer: a directory of a repository at a commit, on its Code,
143
+ // Search, Treemap, Compare, Commits, Issues, Pull requests, Agents, Pulse
144
+ // or Replay tab, `/repo/<repo id>/<commit>[/<tab>…][?path=<directory>]`,
145
+ // the repository's root where it names no directory. Code, the tab a bare
146
+ // link opens, names its file by number and its lines in the fragment, as a
147
+ // bundle's Code does: `/code/<file>#L4`. Compare names the commit compared
148
+ // with, then its mode past the Overview, as an npm version's does:
149
+ // `/compare/<commit>/code`. Without a commit,
150
+ // `/repo/<repo id>[?path=<directory>]`, it opens on the default
151
+ // branch's head, or on the revision a route held in memory names (`ref`),
152
+ // and its route then names the commit it opened. Links from before it moved
153
+ // name it under `/manage/code/`, and open the same.
154
+ export const REPOSITORY_CODE_TABS = new Set(['code', 'search', 'treemap', 'compare', 'commits', 'issues', 'pulls', 'agents', 'pulse', 'replay'])
155
+ // The tabs reading none of the directory's files, which a directory at a
156
+ // commit shows alone.
157
+ export const REPOSITORY_FILELESS_TABS = new Set(['commits', 'issues', 'pulls', 'agents', 'pulse', 'replay'])
158
+ const COMMIT = /^[a-f\d]{40}$/u
159
+ const MAX_DIRECTORY = 500
160
+ const isRepoId = value => Number.isSafeInteger(value) && value > 0
161
+
162
+ // A directory as the server takes one (server-managed/repo-path.ts), in its
163
+ // canonical form: `''` the root, else names with no empty, `.` or `..` one
164
+ // among them, nor control characters, backslashes or spaces around one.
165
+ export function isRepositoryDirectory(path) {
166
+ return typeof path === 'string' && (path === '' || path.length <= MAX_DIRECTORY && !/[\p{Cc}\\]/u.test(path)
167
+ && path.split('/').every(part => part !== '' && part !== '.' && part !== '..' && part === part.trim()))
168
+ }
169
+
170
+ function repositoryCodePath(route) {
171
+ const tab = route.bundleTab ?? 'code'
172
+ const path = route.path ?? ''
173
+ if (!isRepoId(route.repoId) || (route.commit != null && !COMMIT.test(route.commit)) || !isRepositoryDirectory(path) || !REPOSITORY_CODE_TABS.has(tab)
174
+ || (route.compareCommit != null && !COMMIT.test(route.compareCommit))) return null
175
+ const directory = path ? `?path=${path.split('/').map(encodeURIComponent).join('/')}` : ''
176
+ if (route.commit == null) return `/repo/${route.repoId}${directory}`
177
+ const file = tab === 'code' && isLineNumber(route.file) ? `/code/${route.file}` : tab === 'code' ? '' : `/${tab}`
178
+ const compare = tab === 'compare' && route.compareCommit != null ? `/${route.compareCommit}${compareModeSuffix(route.compareMode)}` : ''
179
+ const lines = tab === 'code' && isLineNumber(route.file) && isLineNumber(route.line)
180
+ ? `#L${route.line}${isLineNumber(route.endLine) && route.endLine > route.line ? `-L${route.endLine}` : ''}` : ''
181
+ return `/repo/${route.repoId}/${route.commit}${file}${compare}${directory}${lines}`
182
+ }
183
+
184
+ function parseRepositoryCodeRoute(url) {
185
+ const match = /^\/(?:repo|manage\/code)\/([1-9]\d*)(?:\/([a-f\d]{40})(?:\/(code|search|treemap|compare|commits|history|issues|pulls|agents|pulse|replay)(?:\/([^/]+)(?:\/([a-z]+))?)?)?)?$/u
186
+ .exec(url.pathname.replace(/\/$/u, ''))
187
+ if (!match) return undefined
188
+ const repoId = Number(match[1])
189
+ const path = url.searchParams.get('path') ?? ''
190
+ // Commits' tab was History's, `/history`, which links still name.
191
+ const tab = match[3] === 'history' ? 'commits' : match[3] ?? 'code'
192
+ if (!isRepoId(repoId) || !isRepositoryDirectory(path)) return null
193
+ const route = { view: 'repository-code', repoId, commit: match[2] ?? null, path, bundleTab: tab }
194
+ if (match[4] == null) return route
195
+ if (tab === 'compare') {
196
+ return COMMIT.test(match[4]) && (match[5] == null || COMPARE_MODES.has(match[5])) ? { ...route, compareCommit: match[4], ...compareModeField(match[5]) } : null
197
+ }
198
+ const file = Number(match[4])
199
+ return tab === 'code' && match[5] == null && /^[1-9]\d*$/u.test(match[4]) && Number.isSafeInteger(file) ? { ...route, file, ...codeLines(url.hash) } : null
200
+ }
201
+
131
202
  function isLineNumber(value) {
132
203
  return Number.isSafeInteger(value) && value > 0
133
204
  }
@@ -144,6 +215,12 @@ export function parseManagedRoute(url) {
144
215
  const path = url.pathname.replace(/\/$/u, '') || '/'
145
216
  if (path === '/' || path === '/index.html') return { view: 'home' }
146
217
  if (path === '/my/reviews') return { view: 'reviews' }
218
+ // Linking an account lands here, naming why GitHub's answer was refused,
219
+ // which the page shows once: the address it settles on drops it.
220
+ if (path === '/my/profile') {
221
+ const linkError = url.searchParams.get('link-error')
222
+ return { view: 'profile', ...(linkError && /^[a-z-]{1,64}$/u.test(linkError) ? { linkError } : {}) }
223
+ }
147
224
  const view = Object.keys(MANAGED_PAGES).find(key => MANAGED_PAGES[key] === path)
148
225
  if (view) {
149
226
  const createRepoId = Number(url.searchParams.get('createRepo'))
@@ -169,8 +246,12 @@ export function parseManagedRoute(url) {
169
246
  }
170
247
  const npm = parseNpmRoute(path, url.hash)
171
248
  if (npm !== undefined) return npm
172
- const contentList = /^\/team\/([A-Za-z0-9_-]+)\/(reports|bundles)$/u.exec(path)
249
+ const code = parseRepositoryCodeRoute(url)
250
+ if (code !== undefined) return code
251
+ const contentList = /^\/team\/([A-Za-z0-9_-]+)\/(reports|bundles|repositories|dependencies)$/u.exec(path)
173
252
  if (contentList) return { view: `workspace-${contentList[2]}`, teamSlug: contentList[1] }
253
+ const dependencies = /^\/team\/([A-Za-z0-9_-]+)\/dependencies\/([a-z]+)$/u.exec(path)
254
+ if (dependencies) return DEPENDENCY_TABS.has(dependencies[2]) ? { view: 'workspace-dependencies', teamSlug: dependencies[1], dependencyTab: dependencies[2] } : null
174
255
  const match = /^\/team\/([^/]+)(?:\/report\/([^/]+))?(?:\/(files)|\/finding\/([^/]+))?$/u.exec(path)
175
256
  if (!match) return null
176
257
  try {
@@ -187,9 +268,9 @@ export function parseManagedRoute(url) {
187
268
  // URL tokens are exact server-assigned slugs. Application state and API calls
188
269
  // continue to use UUIDs; never guess IDs from suffixes or accept ID aliases.
189
270
  export function resolveManagedRoute(route, teams, adminBundles = []) {
190
- if (route.view === 'workspace-reports' || route.view === 'workspace-bundles') {
271
+ if (WORKSPACE_CONTENT_VIEWS.has(route.view)) {
191
272
  const matches = teams.filter(team => team.slug === route.teamSlug)
192
- return matches.length === 1 ? { view: route.view, teamId: matches[0].id } : null
273
+ return matches.length === 1 ? { view: route.view, teamId: matches[0].id, ...dependencyTabField(route) } : null
193
274
  }
194
275
  if (route.view === 'bundles') {
195
276
  const { teamSlug, bundleSlug, compareSlug, compareMode, ...rest } = route
@@ -220,9 +301,9 @@ export function resolveManagedRoute(route, teams, adminBundles = []) {
220
301
  }
221
302
 
222
303
  export function managedRouteForIds(route, teams, adminBundles = []) {
223
- if (route.view === 'workspace-reports' || route.view === 'workspace-bundles') {
304
+ if (WORKSPACE_CONTENT_VIEWS.has(route.view)) {
224
305
  const team = teams.find(entry => entry.id === route.teamId)
225
- const result = { view: route.view, teamSlug: team?.slug }
306
+ const result = { view: route.view, teamSlug: team?.slug, ...dependencyTabField(route) }
226
307
  return team?.slug && resolveManagedRoute(result, teams) ? result : null
227
308
  }
228
309
  if (route.view === 'bundles') {
@@ -1,2 +1,5 @@
1
- export type TeamAppMetadata = { appMode: true; appFindings: number } | { appMode: false }
2
- export function managedTeamAppMetadata(workspace: readonly { filename: string; data: unknown }[] & { links?: readonly (readonly string[])[] }): TeamAppMetadata
1
+ // Set for a team that is not App: `findings`, its rows (with `countFindings`),
2
+ // and `appReports`, set by the managed catalog: how many reports open in App mode alone.
3
+ export type TeamAppMetadata = { appMode: true; appFindings: number } | { appMode: false; appReports?: number; findings?: number }
4
+ export function managedTeamAppMetadata(workspace: readonly { filename: string; data: unknown }[] & { links?: readonly (readonly string[])[] },
5
+ options?: { countFindings?: boolean }): TeamAppMetadata
@@ -8,7 +8,7 @@ import { workspaceAppMetadata } from '../workspace-app.js'
8
8
  // its `links`. The managed server classifies each team before sending its
9
9
  // catalog, so the sidebar opens a team collapsed to its App findings at once.
10
10
  // The server backfills finding IDs before building these envelopes.
11
- export function managedTeamAppMetadata(workspace) {
11
+ export function managedTeamAppMetadata(workspace, options = {}) {
12
12
  const duplicates = new Map(), reports = []
13
13
  collectDuplicates(workspace.links ?? [], duplicates)
14
14
  for (const { filename, data } of workspace) {
@@ -27,5 +27,5 @@ export function managedTeamAppMetadata(workspace) {
27
27
  }))
28
28
  reports.push({ fileName: filename, groups })
29
29
  }
30
- return workspaceAppMetadata(reports, id => [...duplicates.get(id) ?? []])
30
+ return workspaceAppMetadata(reports, id => [...duplicates.get(id) ?? []], options)
31
31
  }
@@ -26,6 +26,9 @@ export interface ManagedSessionInfo {
26
26
  user: { id: string; login: string; name: string | null; role: string }
27
27
  csrfToken: string
28
28
  viewer?: { id: string; login: string; name: string | null }
29
+ // Where the user publishes dependency reviews to some team: an npm
30
+ // version's Review tab is theirs (server-managed/dependency-reviews.ts).
31
+ dependencyReview?: true
29
32
  }
30
33
 
31
34
  // The `server-info` frame payload (the `type` discriminant is added at the
@@ -24,11 +24,14 @@ function onlyUnjudgedImports(groups) {
24
24
 
25
25
  // Evaluate the entire workspace, independent of triage, filters, or the lens
26
26
  // left active by another report. Use the same merge and link rules as the view.
27
- export function workspaceAppMetadata(reports, duplicatesOf = () => []) {
27
+ // `countFindings` has a workspace that is not App count its rows too, after
28
+ // the same merging, for a caller that shows it collapsed (managed teams).
29
+ export function workspaceAppMetadata(reports, duplicatesOf = () => [], { countFindings = false } = {}) {
28
30
  const { groups, conflicts } = mergeReportGroups(reports, { hideRuledOut: true })
29
- if (conflicts.size > 0) return { appMode: false }
30
31
  const linked = mergeLinkedWorkspaceGroups(groups, duplicatesOf, appTabs)
31
- if (!onlyUnjudgedImports(linked) && !canLockConfirmed(linked, { tabs: appTabs, kindOf: revalidateKindOf, severityMode: 'corrected' })) return { appMode: false }
32
+ const notApp = countFindings ? { appMode: false, findings: linked.length } : { appMode: false }
33
+ if (conflicts.size > 0) return notApp
34
+ if (!onlyUnjudgedImports(linked) && !canLockConfirmed(linked, { tabs: appTabs, kindOf: revalidateKindOf, severityMode: 'corrected' })) return notApp
32
35
  return {
33
36
  appMode: true,
34
37
  appFindings: linked.filter((group) => appTabs(group).some((f) => f.isApp)).length,