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

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 (71) hide show
  1. package/common/managed/agent-tasks.ts +5 -3
  2. package/common/managed/github-agents.ts +28 -10
  3. package/out/activity.js +4 -0
  4. package/out/chunk-5AHIQFFG.js +36 -0
  5. package/out/chunk-6ANSHRFQ.js +6 -0
  6. package/out/chunk-6WF3KNTL.js +3 -0
  7. package/out/chunk-7BKKYVWR.js +7 -0
  8. package/out/{chunk-7XRR4OFU.js → chunk-FH24VHSJ.js} +1 -1
  9. package/out/chunk-NOZJULWS.js +52 -0
  10. package/out/{chunk-OKRLLE5I.js → chunk-PWFUOLZK.js} +1 -1
  11. package/out/chunk-S6U2J62U.js +232 -0
  12. package/out/{chunk-G24A42V6.js → chunk-SPEFPML6.js} +1 -1
  13. package/out/chunk-UENXT2HK.js +3 -0
  14. package/out/chunk-VL43BTSO.js +1 -0
  15. package/out/{chunk-3PS4KWD2.js → chunk-WCPJDAYA.js} +1 -1
  16. package/out/chunk-ZCS4UYHI.js +1 -0
  17. package/out/chunk-ZGNMDGSB.js +1 -0
  18. package/out/client-managed-import.js +1 -1
  19. package/out/client-managed.js +22 -22
  20. package/out/client-sync.js +2 -2
  21. package/out/compare.js +14 -14
  22. package/out/dialogs.js +40 -40
  23. package/out/graph.js +11 -11
  24. package/out/index.html +11 -11
  25. package/out/scan.js +2 -2
  26. package/out/terminal.js +6 -6
  27. package/out/view.css +1 -1
  28. package/out/view.js +15 -15
  29. package/package.json +2 -1
  30. package/risk/index.d.ts +4 -2
  31. package/risk/npm/assess.js +15 -4
  32. package/risk/npm/registry.js +6 -1
  33. package/server-managed/activity.ts +24 -11
  34. package/server-managed/app-agents.ts +10 -21
  35. package/server-managed/bundle-build-worker.js +7 -3
  36. package/server-managed/bundle-build.ts +5 -3
  37. package/server-managed/bundle-cache.ts +9 -3
  38. package/server-managed/bundle.ts +3 -4
  39. package/server-managed/commit-coauthors.ts +5 -9
  40. package/server-managed/db-methods.ts +286 -149
  41. package/server-managed/db-neon.ts +11 -4
  42. package/server-managed/http.ts +75 -33
  43. package/server-managed/link-reports.ts +5 -1
  44. package/server-managed/npm-kept.ts +19 -5
  45. package/server-managed/npm-loads.ts +27 -15
  46. package/server-managed/npm-packages.ts +18 -11
  47. package/server-managed/npm-risk.ts +64 -17
  48. package/server-managed/pretty-print.ts +45 -12
  49. package/server-managed/report-store.ts +16 -11
  50. package/server-managed/repository-agents.ts +1 -1
  51. package/server-managed/repository-browser.ts +4 -2
  52. package/server-managed/repository-code.ts +28 -11
  53. package/server-managed/repository-pulse.ts +1 -1
  54. package/server-managed/solidity-entry-points.ts +5 -3
  55. package/server-managed/sql-postgres.ts +3 -0
  56. package/server-managed/sql.ts +2 -1
  57. package/server-managed/storage-db.ts +11 -3
  58. package/server-managed/storage-encryption.ts +7 -4
  59. package/server-managed/storage.ts +4 -2
  60. package/server-managed/team-feed.ts +3 -1
  61. package/server-managed/team-reports.ts +6 -1
  62. package/server-managed/uploads.ts +29 -9
  63. package/out/chunk-4UFAMJAX.js +0 -1
  64. package/out/chunk-BOIXMUU5.js +0 -3
  65. package/out/chunk-CC4OM6TZ.js +0 -3
  66. package/out/chunk-JKGQ3O7O.js +0 -6
  67. package/out/chunk-LE5ZWLG4.js +0 -235
  68. package/out/chunk-MSION3G4.js +0 -36
  69. package/out/chunk-S6GIXWQW.js +0 -52
  70. package/out/chunk-V3KWNNMY.js +0 -1
  71. package/out/chunk-WQ74ZBZF.js +0 -7
@@ -14,8 +14,15 @@
14
14
  // instance, or this one restarted, asks npm's downloads API, which answers
15
15
  // some 40 lookups a minute, none of them again that week, each removed a
16
16
  // day past it (npm-kept.ts pruneKept); a private one's in memory.
17
- // - `risk/npm/packages`, `risk/npm/public` and `risk/npm/advisories`, kept an
18
- // hour or a day by risk itself: in memory.
17
+ // - `risk/npm/packages`, `risk/npm/old-versions`, `risk/npm/public` and
18
+ // `risk/npm/advisories`, which risk asks again after an hour: a public
19
+ // package's in memory and in the database too, read from memory first, so
20
+ // that a cold instance, as a serverless one starts, reads no whole document
21
+ // of npm's again that one before it read within the hour; by a hash of
22
+ // their type and key, as a dependency's spec runs to any length, each
23
+ // removed a day on, and one past 256 KiB as JSON (a package's deprecated
24
+ // versions, its messages and all, run long) in memory alone. A private
25
+ // one's in memory.
19
26
  // - `npm/tarballs`: none asked, as tarballs are read as the viewer's loads
20
27
  // read them, kept where they keep them (npm-loads.ts loadNpmTarball), and
21
28
  // an assessment kept needs none.
@@ -69,11 +76,20 @@ function remember(key: string, value: unknown) {
69
76
  while (memory.size > MEMORY_ENTRIES || memoryLength > MEMORY_LENGTH) forget(memory.keys().next().value!)
70
77
  }
71
78
 
72
- const hashed = (type: string, key: string) => `risk/${createHash('sha256').update(`${type}\0${key}`).digest('hex')}`
73
- // What of a public package is kept in the database, and for how long its
74
- // downloads are: risk's week, and a day more.
79
+ const digest = (type: string, key: string) => createHash('sha256').update(`${type}\0${key}`).digest('hex')
80
+ const hashed = (type: string, key: string) => `risk/${digest(type, key)}`
81
+ // What of a public package is kept in the database by its key, and for how
82
+ // long its downloads are: risk's week, and a day more.
75
83
  const IN_DATABASE = new Set(['risk/npm/versions', 'risk/npm/downloads'])
76
84
  const DOWNLOADS_KEPT_MS = 8 * 24 * 60 * 60_000
85
+ // What of a public package risk asks again after an hour, kept in memory and
86
+ // in the database (HOURLY_PREFIX), each a day, none past HOURLY_LENGTH as
87
+ // JSON there.
88
+ const HOURLY = new Set(['risk/npm/packages', 'risk/npm/old-versions', 'risk/npm/public', 'risk/npm/advisories'])
89
+ const HOURLY_PREFIX = 'risk/hourly/'
90
+ const HOURLY_KEPT_MS = 24 * 60 * 60_000
91
+ const HOURLY_LENGTH = 256 * 1024
92
+ const databaseKey = (type: string, key: string) => HOURLY.has(type) ? `${HOURLY_PREFIX}${digest(type, key)}` : `${type}/${key}`
77
93
 
78
94
  export interface NpmRiskStores {
79
95
  db: UpstreamCacheStore
@@ -82,10 +98,13 @@ export interface NpmRiskStores {
82
98
  }
83
99
 
84
100
  // A cache that reads and writes many of a type at once too, as risk's
85
- // readying of downloads asks (npmReadyDownloads): one read and one write of
86
- // the database each, not one a key, as a request's queries take turns on its
87
- // connection (db-neon.ts).
101
+ // readying of downloads asks (npmReadyDownloads), and reads records of any
102
+ // types at once, as risk reads a package's record and its versions'
103
+ // (registry.js npmPackageFacts): one read and one write of the database each,
104
+ // not one a key, as a request's queries take turns on its connection
105
+ // (db-neon.ts).
88
106
  export interface NpmRiskCache extends CacheStore {
107
+ readEach(entries: [type: string, key: string][]): Promise<unknown[]>
89
108
  readMany(type: string, keys: string[]): Promise<Map<string, unknown>>
90
109
  writeMany(type: string, entries: [key: string, value: unknown][]): Promise<void>
91
110
  }
@@ -94,13 +113,32 @@ export interface NpmRiskCache extends CacheStore {
94
113
  // that what is kept of it can be shared.
95
114
  export function npmRiskCache({ db, npmCache, debug }: NpmRiskStores, isPublic: boolean): NpmRiskCache {
96
115
  const warn = (what: string, error: unknown) => { if (debug) console.warn(`managed: risk cache ${what} failed:`, error) }
116
+ const memoryKey = (type: string, key: string) => `${isPublic ? '' : 'private\0'}${type}\0${key}`
117
+ // Whether a record of `type` is read of the database: unless it is one
118
+ // kept in memory too (HOURLY) and there.
119
+ const ofDatabase = (type: string, key: string) => isPublic && (IN_DATABASE.has(type) || HOURLY.has(type) && !memory.has(memoryKey(type, key)))
120
+ // A record as the database keeps it, read: an hourly one kept in memory
121
+ // too, for the next to ask.
122
+ const readOf = (type: string, key: string, row: string | null | undefined) => {
123
+ const value: unknown = row == null ? null : JSON.parse(row)
124
+ if (value !== null && HOURLY.has(type)) remember(memoryKey(type, key), value)
125
+ return value
126
+ }
97
127
  const cache: NpmRiskCache = {
128
+ async readEach(entries) {
129
+ const asked = new Set(entries.flatMap(([type, key], i) => ofDatabase(type, key) ? [i] : []))
130
+ let rows = new Map<string, string>()
131
+ if (asked.size > 0) {
132
+ try { rows = await db.getUpstreamCacheEntries([...asked].map(i => databaseKey(...entries[i]!))) } catch (error) { warn('read', error) }
133
+ }
134
+ return Promise.all(entries.map(([type, key], i) => {
135
+ if (!asked.has(i)) return cache.read(type, key)
136
+ try { return readOf(type, key, rows.get(databaseKey(type, key))) } catch (error) { warn('read', error); return null }
137
+ }))
138
+ },
98
139
  async readMany(type, keys) {
99
- if (!isPublic || !IN_DATABASE.has(type)) return new Map(await Promise.all(keys.map(async key => [key, await cache.read(type, key)] as const)))
100
- try {
101
- const found = await db.getUpstreamCacheEntries(keys.map(key => `${type}/${key}`))
102
- return new Map(keys.map(key => { const value = found.get(`${type}/${key}`); return [key, value === undefined ? null : JSON.parse(value)] }))
103
- } catch (error) { warn('read', error); return new Map() }
140
+ const values = await cache.readEach(keys.map(key => [type, key]))
141
+ return new Map(keys.map((key, i) => [key, values[i]]))
104
142
  },
105
143
  async writeMany(type, entries) {
106
144
  if (!isPublic || !IN_DATABASE.has(type)) { for (const [key, value] of entries) await cache.write(type, key, value); return }
@@ -112,22 +150,31 @@ export function npmRiskCache({ db, npmCache, debug }: NpmRiskStores, isPublic: b
112
150
  if (type === 'npm/tarballs') return null
113
151
  try {
114
152
  if (isPublic && type === 'risk/npm/assessments' && npmCache) return JSON.parse((await buffer((await npmCache.open(hashed(type, key))).stream)).toString('utf8'))
115
- if (isPublic && IN_DATABASE.has(type)) { const value = await db.getUpstreamCacheEntry(`${type}/${key}`); return value === null ? null : JSON.parse(value) }
153
+ if (ofDatabase(type, key)) return readOf(type, key, await db.getUpstreamCacheEntry(databaseKey(type, key)))
116
154
  } catch (error) { if (!(error instanceof CacheMissError)) warn('read', error); return null }
117
- return memory.get(`${isPublic ? '' : 'private\0'}${type}\0${key}`)?.value ?? null
155
+ return memory.get(memoryKey(type, key))?.value ?? null
118
156
  },
119
157
  async write(type, key, value) {
120
158
  if (type === 'npm/tarballs') return
159
+ if (isPublic && HOURLY.has(type)) remember(memoryKey(type, key), value)
121
160
  try {
122
161
  if (isPublic && type === 'risk/npm/assessments' && npmCache) { await npmCache.put(hashed(type, key), Buffer.from(JSON.stringify(value))); return }
123
162
  if (isPublic && IN_DATABASE.has(type)) {
124
163
  const at = Date.now()
125
- await db.setUpstreamCacheEntry(`${type}/${key}`, JSON.stringify(value), at)
164
+ await db.setUpstreamCacheEntry(databaseKey(type, key), JSON.stringify(value), at)
126
165
  if (type === 'risk/npm/downloads') await pruneKept(db, `${type}/`, DOWNLOADS_KEPT_MS, at, error => { warn('prune', error) })
127
166
  return
128
167
  }
168
+ if (isPublic && HOURLY.has(type)) {
169
+ const json = JSON.stringify(value)
170
+ if (json.length > HOURLY_LENGTH) return
171
+ const at = Date.now()
172
+ await db.setUpstreamCacheEntry(databaseKey(type, key), json, at)
173
+ await pruneKept(db, HOURLY_PREFIX, HOURLY_KEPT_MS, at, error => { warn('prune', error) })
174
+ return
175
+ }
129
176
  } catch (error) { warn('write', error); return }
130
- remember(`${isPublic ? '' : 'private\0'}${type}\0${key}`, value)
177
+ remember(memoryKey(type, key), value)
131
178
  },
132
179
  }
133
180
  return cache
@@ -45,6 +45,10 @@ const MAX_KEPT_DETAILS_BYTES = 256 * 1024 * 1024
45
45
  // Files hashed at once: one at a time, each waits its own round trip to the
46
46
  // thread pool; all at once, every one's job is held together.
47
47
  const HASHES_AT_ONCE = 32
48
+ // Kept copies a stream reads ahead of the line it is sending (readAhead):
49
+ // one at a time, each waits its own storage round trip; each read whole, as
50
+ // its line will send it.
51
+ const KEPT_AHEAD = 8
48
52
 
49
53
  export class PrettyError extends Error {
50
54
  status: number
@@ -166,6 +170,29 @@ async function prettyLine({ path, hash }: PrettyRequest, copy: () => Promise<Ope
166
170
  }
167
171
  }
168
172
 
173
+ // A file's line from its kept copy, `open` opening it, or null where none is
174
+ // kept.
175
+ async function keptLine(request: PrettyRequest, open: () => Promise<OpenedBlob | null>): Promise<PrettyLine | null> {
176
+ const copy = await open()
177
+ return copy && prettyLine(request, () => Promise.resolve(copy))
178
+ }
179
+
180
+ // Each of `items` with what `read` reads of it, in their order, up to
181
+ // KEPT_AHEAD of them read at once ahead of the one iterated, each answered
182
+ // (or failing) in its turn. One iterated no further starts no more; those
183
+ // under way settle unread.
184
+ async function* readAhead<T, R>(items: readonly T[], read: (item: T) => Promise<R>): AsyncGenerator<[T, R]> {
185
+ const reads: Promise<R>[] = []
186
+ for (const [i, item] of items.entries()) {
187
+ while (reads.length < Math.min(items.length, i + KEPT_AHEAD)) {
188
+ const reading = read(items[reads.length]!)
189
+ reading.catch(() => {})
190
+ reads.push(reading)
191
+ }
192
+ yield [item, await reads[i]!]
193
+ }
194
+ }
195
+
169
196
  // The files that changed between two sides, pretty-printed: each side's
170
197
  // formattable files the other has another text at the path of, or, where it
171
198
  // has none there, none of their text at a path of its own (a file renamed, or
@@ -174,8 +201,9 @@ async function prettyLine({ path, hash }: PrettyRequest, copy: () => Promise<Ope
174
201
  // the viewer diffs but where it pairs files otherwise (among copies of one
175
202
  // text, across packages), whose copies it asks for alone. As they are
176
203
  // iterated, in path order, the base's first where both have one: those kept,
177
- // at once; then the rest, formatted one after another, the minified before
178
- // the others (needsPrettyPrint), as the viewer prints those unasked.
204
+ // at once, a few read ahead (readAhead); then the rest, formatted one after
205
+ // another, the minified before the others (needsPrettyPrint), as the viewer
206
+ // prints those unasked.
179
207
  export function compared(sides: readonly [ComparedSide, ComparedSide]): AsyncGenerator<ComparedLine> {
180
208
  // Each side's texts at the paths the other hasn't: where a file moved has its own.
181
209
  const moved = sides.map((own, i) => new Set([...own.hashes].filter(([path]) => !sides[1 - i]!.hashes.has(path)).map(([, hash]) => hash)))
@@ -185,9 +213,8 @@ export function compared(sides: readonly [ComparedSide, ComparedSide]): AsyncGen
185
213
  }).toSorted((a, b) => (a.file.path < b.file.path ? -1 : a.file.path > b.file.path ? 1 : 0))
186
214
  return (async function* () {
187
215
  const unkept: typeof changed = []
188
- for (const { own, file } of changed) {
189
- const copy = await own.kept(file)
190
- if (copy) yield { side: own.side, ...await prettyLine(file, () => Promise.resolve(copy)) }
216
+ for await (const [{ own, file }, line] of readAhead(changed, item => keptLine(item.file, () => item.own.kept(item.file)))) {
217
+ if (line) yield { side: own.side, ...line }
191
218
  else unkept.push({ own, file })
192
219
  }
193
220
  const minified = new Map(unkept.map(({ file }) => [file, needsPrettyPrint(file.path, file.text)]))
@@ -325,15 +352,18 @@ export function createPrettyCache(bundles: BundleCacheStorage, npm: CacheStorage
325
352
  return body
326
353
  })
327
354
  }
355
+ // The copy kept for a public version's file, or null where none is (and
356
+ // for a private version's, which none is kept for).
357
+ const npmKept = (doc: NpmVersionDocument, request: PrettyRequest) => (doc.private ? Promise.resolve(null) : kept(() => npm.open(prettyFile(request))))
328
358
  // A version's file pretty-printed, `read` reading its text (and checking
329
359
  // it is the file named): from the copy kept for a public version, else
330
360
  // made, and kept for a public one.
331
361
  async function npmCopy(doc: NpmVersionDocument, request: PrettyRequest, read: () => Promise<string>): Promise<OpenedBlob> {
362
+ return await npmKept(doc, request) ?? npmMade(doc, request, read)
363
+ }
364
+ // The file pretty-printed from the text `read` reads, kept for a public version.
365
+ async function npmMade(doc: NpmVersionDocument, request: PrettyRequest, read: () => Promise<string>): Promise<OpenedBlob> {
332
366
  const file = prettyFile(request)
333
- if (!doc.private) {
334
- try { return await npm.open(file) }
335
- catch (err) { if (!(err instanceof CacheMissError)) throw err }
336
- }
337
367
  return opened(await shared(doc.private ? `npm ${doc.dist.integrity} ${file}` : `npm ${file}`, async () => {
338
368
  const body = await prettyBody(await read(), request.extension)
339
369
  if (!doc.private) await keep(() => npm.put(file, body))
@@ -369,11 +399,14 @@ export function createPrettyCache(bundles: BundleCacheStorage, npm: CacheStorage
369
399
  },
370
400
  // Every file of the version that needs pretty-printing (npmPrintable),
371
401
  // read before this resolves; then, as they are iterated, each
372
- // pretty-printed as `npm` has it, one after another.
402
+ // pretty-printed as `npm` has it, one after another, those kept a few
403
+ // read ahead (readAhead).
373
404
  async npmAll(doc: NpmVersionDocument): Promise<AsyncGenerator<PrettyLine>> {
374
405
  const files = await npmPrintable(doc)
375
406
  return (async function* () {
376
- for (const { text, ...request } of files) yield await prettyLine(request, () => npmCopy(doc, request, () => Promise.resolve(text)))
407
+ for await (const [file, line] of readAhead(files, request => keptLine(request, () => npmKept(doc, request)))) {
408
+ yield line ?? await prettyLine(file, () => npmMade(doc, file, () => Promise.resolve(file.text)))
409
+ }
377
410
  })()
378
411
  },
379
412
  // Two bundles' sides, or two versions', as a comparison reads them, read
@@ -392,7 +425,7 @@ export function createPrettyCache(bundles: BundleCacheStorage, npm: CacheStorage
392
425
  npmSides(base: NpmVersionDocument, other: NpmVersionDocument): Promise<[ComparedSide, ComparedSide]> {
393
426
  const sideOf = async (doc: NpmVersionDocument, side: Side): Promise<ComparedSide> => ({ side, ...await sideFiles(await npmFiles(doc), (_, bytes) => npmFileHash(bytes)),
394
427
  packageDirs: null,
395
- kept: request => (doc.private ? Promise.resolve(null) : kept(() => npm.open(prettyFile(request)))),
428
+ kept: request => npmKept(doc, request),
396
429
  made: (request, text) => npmCopy(doc, request, () => Promise.resolve(text)) })
397
430
  return Promise.all([sideOf(base, 'base'), sideOf(other, 'other')])
398
431
  },
@@ -32,9 +32,7 @@ export function createReportStore(legacy: BlobStore, compressed: BlobStore,
32
32
  if (compressedOnly.size >= COMPRESSED_ONLY_MAX) compressedOnly.delete(compressedOnly.values().next().value!)
33
33
  compressedOnly.add(id)
34
34
  }
35
- async function openCompressed(id: string): Promise<OpenedBlob | null> {
36
- const stored = await compressed.open(id)
37
- if (!stored) return null
35
+ function decoded(stored: OpenedBlob): OpenedBlob {
38
36
  const stream = createBrotliDecompress()
39
37
  // pipeline carries storage/authentication errors into the decoded stream
40
38
  // and closes the stored source when a reader leaves before EOF.
@@ -43,22 +41,29 @@ export function createReportStore(legacy: BlobStore, compressed: BlobStore,
43
41
  }
44
42
  async function open(id: string): Promise<OpenedBlob | null> {
45
43
  if (compressedOnly.has(id)) {
46
- const stored = await openCompressed(id)
47
- if (stored) return stored
44
+ const stored = await compressed.open(id)
45
+ if (stored) return decoded(stored)
48
46
  compressedOnly.delete(id)
49
47
  }
50
48
  // Keep the original available until conversion succeeds. Concurrent reads
51
49
  // can finish using an already-open stream while another reader converts it.
52
- const original = await legacy.open(id)
53
- if (original) {
54
- const bytes = await readBytes(original)
50
+ // Both copies are asked for at once, the original answering where it is
51
+ // still there: one round trip of storage for a report not known yet, as
52
+ // a cold instance knows none.
53
+ const [original, stored] = await Promise.allSettled([legacy.open(id), compressed.open(id)])
54
+ const unused = (copy: PromiseSettledResult<OpenedBlob | null>) => { if (copy.status === 'fulfilled') copy.value?.stream.destroy() }
55
+ if (original.status === 'rejected') { unused(stored); throw original.reason }
56
+ if (original.value) {
57
+ unused(stored)
58
+ const bytes = await readBytes(original.value)
55
59
  await replaceLegacy(id, await encodeBrotli(bytes, 9))
56
60
  remember(id)
57
61
  return { size: bytes.length, stream: Readable.from([bytes]) }
58
62
  }
59
- const stored = await openCompressed(id)
60
- if (stored) remember(id)
61
- return stored
63
+ if (stored.status === 'rejected') throw stored.reason
64
+ if (!stored.value) return null
65
+ remember(id)
66
+ return decoded(stored.value)
62
67
  }
63
68
  return {
64
69
  async put(id, bytes) {
@@ -175,7 +175,7 @@ async function agentBranches(owner: string, name: string, prefix: string, token:
175
175
  // What its answers are kept by, with their cursors (http.ts
176
176
  // handleRepositoryAgents): bumped as what a page reads, or how it tells an
177
177
  // agent, changes, so that no answer an older server kept is sent for a day.
178
- export const AGENTS_READ_VERSION = 5
178
+ export const AGENTS_READ_VERSION = 6
179
179
 
180
180
  // A cursor of the default branch's commits: GitHub names one by a commit
181
181
  // and how many on from it, `<oid> <offset>`, a space between.
@@ -272,8 +272,10 @@ async function repositoryReader(repo: SelectedRepo, userToken: string | null, in
272
272
  const manifestPath = path ? `${path}/package.json` : 'package.json'
273
273
  const manifest = data.find(item => item?.path === manifestPath && item.name === 'package.json' && item.type === 'file' && !item.submodule_git_url
274
274
  && typeof item.size === 'number' && item.size >= 0 && item.size <= MAX_PACKAGE_BYTES && typeof item.sha === 'string' && /^[a-f\d]{40}$/iu.test(item.sha))
275
- const packageEntryPoints = manifest ? await readPackageEntryPoints(manifest.sha, path, read) : undefined
276
- const solidity = await readSolidityEntryPoints(path, data, read)
275
+ // Both optional suggestions, read together: neither fails, each
276
+ // answering none where its reads do.
277
+ const [packageEntryPoints, solidity] = await Promise.all([manifest ? readPackageEntryPoints(manifest.sha, path, read) : undefined,
278
+ readSolidityEntryPoints(path, data, read)])
277
279
  return { entries, limited: data.length >= 1000, ...(packageEntryPoints ? { packageEntryPoints } : {}),
278
280
  ...(solidity.paths.length > 0 || solidity.limited ? { solidityEntryPoints: solidity.paths, soliditySuggestionsLimited: solidity.limited } : {}),
279
281
  }
@@ -7,24 +7,36 @@
7
7
  // active ones (npm-loads.ts sharedLoad).
8
8
  import { Buffer } from 'node:buffer'
9
9
  import { encodeBrotli } from './brotli.ts'
10
- import { MAX_NPM_PACKAGE_BYTES, MAX_NPM_PACKAGE_FILES, NpmPackageError, npmFileRows } from './npm-packages.ts'
11
- import { type NpmLoad, readTarballFiles, sharedLoad } from './npm-loads.ts'
10
+ import { MAX_NPM_JSON_LENGTH, NpmPackageError, npmFileRows } from './npm-packages.ts'
11
+ import { type NpmLoad, loadPlaces, readTarballFiles, sharedLoad } from './npm-loads.ts'
12
12
 
13
- // As much as a package may hold: the viewer holds every file in memory.
14
- export const MAX_REPOSITORY_CODE_FILES = MAX_NPM_PACKAGE_FILES
15
- export const MAX_REPOSITORY_CODE_BYTES = MAX_NPM_PACKAGE_BYTES
13
+ // What a directory may hold, bounded apart from a package's (npm-packages.ts),
14
+ // as the viewer holds every file in memory: its files' count and bytes.
15
+ export const MAX_REPOSITORY_CODE_FILES = 80_000
16
+ export const MAX_REPOSITORY_CODE_BYTES = 256 * 1024 * 1024
17
+ // The tarball they come in, bounded in proportion as a package's is: its tar
18
+ // stream, headers and padding included, and its entries, as many again as
19
+ // files, as GitHub's has one for each directory too.
20
+ const REPOSITORY_TAR = { maxFiles: MAX_REPOSITORY_CODE_FILES, maxBytes: MAX_REPOSITORY_CODE_BYTES, maxEntries: 2 * MAX_REPOSITORY_CODE_FILES, maxTarBytes: 384 * 1024 * 1024 }
21
+ // What the files may take as the viewer's JSON, written as one string: short
22
+ // of the longest V8 holds (2^29 - 24 characters).
23
+ const MAX_REPOSITORY_CODE_JSON_LENGTH = 384 * 1024 * 1024
16
24
 
17
25
  // Whether a directory's files fit, by GitHub's listing of its tree and every
18
26
  // tree under it (repository-browser.ts repositoryTreeFiles): asked before
19
27
  // its tarball is, which may run to hundreds of MiB. One GitHub cut short
20
28
  // (null) has too many.
21
- export function repositoryCodeFits(files: { files: number; bytes: number } | null): boolean {
29
+ export function repositoryCodeFits(files: RepositoryCodeFiles | null): files is RepositoryCodeFiles {
22
30
  return files !== null && files.files <= MAX_REPOSITORY_CODE_FILES && files.bytes <= MAX_REPOSITORY_CODE_BYTES
23
31
  }
24
32
 
33
+ // A directory's files' count and bytes, as GitHub lists its tree.
34
+ export interface RepositoryCodeFiles { files: number; bytes: number }
35
+
25
36
  // The directory read: the repository by its id and name, the commit, the
26
- // directory (`''` for the root) and its tree's id.
27
- export interface RepositoryCodeInput { repoId: number; repo: string; commit: string; path: string; tree: string }
37
+ // directory (`''` for the root), its tree's id, and its files as listed,
38
+ // which weigh its load among the active ones.
39
+ export interface RepositoryCodeInput { repoId: number; repo: string; commit: string; path: string; tree: string; listed: RepositoryCodeFiles }
28
40
 
29
41
  // A package's refusals, as a directory's: past the bounds, or an archive
30
42
  // @preventive/archive refuses, as it does one holding a symlink that climbs
@@ -39,14 +51,19 @@ const directoryError = (err: unknown) => {
39
51
  // repo, commit, path, tree, files }`, its files as npmFileRows gives them,
40
52
  // by their paths in the directory, from `tarball`, the tree's.
41
53
  export function loadRepositoryCodeBody(input: RepositoryCodeInput, tarball: () => Promise<Uint8Array>): NpmLoad<Buffer> {
42
- const { repoId, repo, commit, path, tree } = input
54
+ const { repoId, repo, commit, path, tree, listed } = input
55
+ // Its places among the active loads, by its files as listed, hold their
56
+ // JSON too, which the listing cannot tell, as escaping may grow a text
57
+ // sixfold: as much as a package's may take for each.
58
+ const places = loadPlaces(listed)
59
+ const maxLength = Math.min(MAX_REPOSITORY_CODE_JSON_LENGTH, places * MAX_NPM_JSON_LENGTH)
43
60
  return sharedLoad(`repository ${repoId} ${repo} ${commit} ${path} ${tree}`, async () => {
44
61
  let files
45
62
  // A symlink may reach the repository's root, as a monorepo package's
46
63
  // LICENSE links the root's, and no further: the directory's depth, less
47
64
  // the level the archive's root already stands above its top directory.
48
- try { files = npmFileRows(await readTarballFiles(await tarball(), { symlinksAbove: path.split('/').length - 1 })) }
65
+ try { files = npmFileRows(await readTarballFiles(await tarball(), { ...REPOSITORY_TAR, symlinksAbove: path.split('/').length - 1 }), maxLength) }
49
66
  catch (err) { throw directoryError(err) }
50
67
  return encodeBrotli(Buffer.from(JSON.stringify({ repoId, repo, commit, path, tree, files })))
51
- }, 'repository-busy')
68
+ }, 'repository-busy', { places })
52
69
  }
@@ -20,7 +20,7 @@ import { ACTOR, type GraphAccount, type HistoryCommit, type HistoryPages, accoun
20
20
  export const PULSE_WEEKS = [1, 4, 13, 52]
21
21
  // What a year counted from its commits holds, its version: kept years of
22
22
  // another are counted again.
23
- export const PULSE_YEAR_VERSION = 8
23
+ export const PULSE_YEAR_VERSION = 9
24
24
  const WEEK_MS = 7 * 24 * 60 * 60_000
25
25
  const DAY_MS = 24 * 60 * 60_000
26
26
  const MAX_CONTRIBUTORS = 100
@@ -49,10 +49,12 @@ export async function readSolidityEntryPoints(directory: string, listing: unknow
49
49
  for (const entry of entries) {
50
50
  if (entry.type === 'file' && sourcePath(entry.path) && addSoliditySuggestion(paths, entry.path)) limited = true
51
51
  }
52
- for (const root of roots) {
53
- if (!sourcePath(`${root.path}/Contract.sol`)) continue
52
+ // Both roots' listings asked together, then read in order.
53
+ const listings = roots.map(root => Promise.resolve().then(() => (sourcePath(`${root.path}/Contract.sol`) ? read(`/git/trees/${root.sha}?recursive=1`) : null)))
54
+ for (const listed of listings) listed.catch(() => {})
55
+ for (const [i, root] of roots.entries()) {
54
56
  try {
55
- const data = await read(`/git/trees/${root.sha}?recursive=1`) as { tree?: unknown; truncated?: unknown }
57
+ const data = await listings[i] as { tree?: unknown; truncated?: unknown } | null
56
58
  if (!Array.isArray(data?.tree)) continue
57
59
  limited ||= data.truncated === true
58
60
  for (const entry of data.tree) {
@@ -10,6 +10,9 @@ export function postgresSql(source: string): { sql: string; names: string[] } {
10
10
  .replaceAll('INSERT OR IGNORE INTO', 'INSERT INTO')
11
11
  .replaceAll('CROSS JOIN', 'JOIN')
12
12
  .replaceAll(/\binstr\(/gu, 'strpos(')
13
+ // Rows gathered as one JSON array value, NULL (not '[]') where none are.
14
+ .replaceAll(/\bjson_group_array\(/gu, 'json_agg(')
15
+ .replaceAll(/\bjson_object\(/gu, 'json_build_object(')
13
16
  // Quoted literals (including ':') are never placeholders or identifiers.
14
17
  sql = sql.split(/('(?:''|[^'])*')/u).map((part, i) => i % 2 ? part : part
15
18
  .replaceAll(/\?|(?<!:):([a-zA-Z]\w*)/gu, (_match, name: string | undefined) => {
@@ -21,8 +21,9 @@ export interface ManagedSqlDriver extends ManagedSql {
21
21
  // Lease and cache writes coordinate through their conditional UPSERT/UPDATE alone.
22
22
  const SINGLE_STATEMENTS = new Set(['getReport', 'getBundle', 'getStorageRow', 'getStorageEncryption', 'getTeam', 'getComment',
23
23
  'userCanReadReport', 'userCanReadRepo', 'userCanReadRepoPath', 'userCanReadBundle', 'reportPermissionsFor', 'listComments', 'listManagedIssues',
24
- 'listTriageHistory', 'listUsers', 'listUserOptions', 'listActivityReports', 'getRepoFullName', 'getSessionWithUser', 'getViewSessionWithUser',
24
+ 'listTriageHistory', 'listUsers', 'listUserOptions', 'listActivityReports', 'getRepoFullName', 'getUser', 'getSessionWithUser', 'getViewSessionWithUser',
25
25
  'listTriageFixes',
26
+ 'sessionWithUser', 'getTeamReportAccessSnapshot', 'getReportAccessSnapshot', 'getReportView', 'getUserGithubId', 'getUserTeamFeedSnapshot', 'getAnnotations', 'getUserTokens',
26
27
  'listReports', 'listBundles', 'listReadableBundleIds', 'listAllRepos', 'listSelectedRepos', 'listRepoScopesForUser', 'listTriage',
27
28
  'getFeedState', 'getWorkspaceShareFeedState', 'claimMaintenanceLease', 'finishMaintenanceLease', 'cacheRepoDefaultBranch', 'releaseBundleBuildLease',
28
29
  'getUpstreamCacheEntry', 'getUpstreamCacheEntries', 'getFirstUpstreamCacheEntries', 'setUpstreamCacheEntry', 'setUpstreamCacheEntries', 'ensureSessionUploadKey',
@@ -47,10 +47,18 @@ export function unwrapDataKey(key: StorageKey, type: StorageRowKind, row: Pick<S
47
47
  return value
48
48
  }
49
49
 
50
+ // What storageState reads of managed_storage_encryption, as `alias` names it,
51
+ // for a statement reading it with more (storageStateOf).
52
+ export const storageStateColumns = (alias: string) => `${alias}.test_value AS testValue, ${alias}.enabled_at AS enabledAt,
53
+ ${alias}.cursor AS cursor, ${alias}.complete AS complete, ${alias}.migrated AS migrated, ${alias}.cleanup_cursor AS cleanupCursor,
54
+ ${alias}.cleanup_found AS cleanupFound, ${alias}.cleanup_complete AS cleanupComplete`
50
55
  export async function storageState(db: ManagedSql, key: StorageKey | null): Promise<StorageEncryptionState | null> {
51
- const row = await db.prepare(`SELECT test_value AS testValue, enabled_at AS enabledAt, cursor, complete, migrated,
52
- cleanup_cursor AS cleanupCursor, cleanup_found AS cleanupFound, cleanup_complete AS cleanupComplete
53
- FROM managed_storage_encryption WHERE id = 1`).get() as (StorageEncryptionState & { testValue: string }) | undefined
56
+ return storageStateOf(await db.prepare(`SELECT ${storageStateColumns('e')} FROM managed_storage_encryption e WHERE e.id = 1`).get() as StoredState | undefined, key)
57
+ }
58
+ type StoredState = StorageEncryptionState & { testValue: string }
59
+ // The state storageStateColumns read, none where its row is missing, its
60
+ // test value checked against `key`.
61
+ export function storageStateOf(row: StoredState | undefined, key: StorageKey | null): StorageEncryptionState | null {
54
62
  if (!row) return null
55
63
  if (!key) throw new Error('Managed storage requires its configured encryption key')
56
64
  const plain = unwrapStorageValue(key, 'managed_storage_encryption:1', row.testValue)
@@ -180,8 +180,10 @@ async function convertStoredReport(raw: RawObjectStorage, db: StorageDb, key: St
180
180
 
181
181
  // Startup enables encryption before constructing stores; validate the persisted
182
182
  // requirement even when this adapter is opened without a key.
183
- async function storagePolicy(db: StorageDb, key: StorageKey | null) {
184
- let enabled = await db.getStorageEncryption()
183
+ // `known` is the state the caller read a moment before, where it found it
184
+ // enabled, so a cold start reads it once.
185
+ async function storagePolicy(db: StorageDb, key: StorageKey | null, known: StorageEncryptionState | null) {
186
+ let enabled = known ?? await db.getStorageEncryption()
185
187
  let checkedAt = Date.now()
186
188
  return async (fresh = false) => {
187
189
  // Enabled is irreversible. Keyless instances cache "off" briefly, while
@@ -196,8 +198,9 @@ async function storagePolicy(db: StorageDb, key: StorageKey | null) {
196
198
  }
197
199
  }
198
200
 
199
- export async function createEncryptedObjectStorage(raw: RawObjectStorage, db: StorageDb, key: StorageKey | null): Promise<ObjectStorage> {
200
- const mode = await storagePolicy(db, key)
201
+ export async function createEncryptedObjectStorage(raw: RawObjectStorage, db: StorageDb, key: StorageKey | null,
202
+ known: StorageEncryptionState | null = null): Promise<ObjectStorage> {
203
+ const mode = await storagePolicy(db, key, known)
201
204
  async function open(identity: string): ReturnType<ObjectStorage['open']> {
202
205
  logicalKey(identity)
203
206
  const state = await mode()
@@ -31,11 +31,13 @@ export async function openManagedStorage(config: ManagedConfig) {
31
31
  const raw = config.neonUrl ? await openVercelObjectStorage(config.blobToken!) : createDiskObjectStorage(dirname(config.dbPath))
32
32
  // Enable before exposing stores or token writers. Concurrent first starts
33
33
  // serialize activation in the database and validate the winning key.
34
- if (key && !await db.getStorageEncryption()) {
34
+ const enabled = key ? await db.getStorageEncryption() : null
35
+ if (key && !enabled) {
35
36
  if (config.vercelPreview) throw new Error('Storage encryption must be enabled outside a Vercel preview deployment')
36
37
  await db.enableStorageEncryption()
37
38
  }
38
- const objects = await createEncryptedObjectStorage(raw, db, key)
39
+ // Enabled already: that state, not read again.
40
+ const objects = await createEncryptedObjectStorage(raw, db, key, enabled)
39
41
  const storage = createManagedStores(objects, !config.neonUrl)
40
42
  // Move legacy team link uploads into encrypted SQL before serving catalogs.
41
43
  // The DB transaction commits first; a failed blob cleanup cannot lose links.
@@ -74,7 +74,9 @@ export async function serveUserTeamFeed(res: ServerResponse, deps: ManagedHttpDe
74
74
  }
75
75
  const catalog = await deps.db.getUserTeamFeedSnapshot(sessionId, Date.now())
76
76
  checkUser(catalog)
77
- checkUser(await deps.db.getReportAccessSnapshot(sessionId, Date.now(), []))
77
+ // The session read again after the catalog, as the catalog is published
78
+ // next: one statement, as the poll's own check is.
79
+ checkUser(await deps.db.getFeedState(sessionId, Date.now()))
78
80
  // Publish catalog state before loading any report blobs. A slow or broken
79
81
  // focused report must not hide membership/content changes from the client.
80
82
  if (!publish('teams', catalog!.revision)) return
@@ -21,6 +21,11 @@ const VISIBILITY_CACHE_MAX_SNAPSHOTS = 32
21
21
  const caches = new WeakMap<ManagedDb, Map<string, Visibility>>()
22
22
  const REPORT_CACHE_BYTES = 16 * 1024 * 1024
23
23
  const reportCaches = new WeakMap<ManagedDb, Map<string, Buffer>>()
24
+ // A workspace's report reads at once. Its reports' sizes are bounded together
25
+ // before any is read, and each is held parsed till the build ends, so more at
26
+ // once costs only the raw bytes in flight: a cold build of a team's reports
27
+ // is so many storage round trips one after another.
28
+ const WORKSPACE_READS_AT_ONCE = 16
24
29
  function retainReports(db: ManagedDb, key: string, bytes: Buffer) {
25
30
  if (bytes.length > REPORT_CACHE_BYTES) return
26
31
  let cache = reportCaches.get(db)
@@ -93,7 +98,7 @@ async function buildTeamWorkspace(db: ManagedDb, store: BlobStore, snapshot: Tea
93
98
  let inputBytes = 0, next = 0
94
99
  // Bound both remote reads and raw buffers, retaining stable report order for
95
100
  // the cross-report classification that follows. Finish workers on failure.
96
- const results = await Promise.allSettled(Array.from({ length: Math.min(4, snapshot.reports.length) }, async () => {
101
+ const results = await Promise.allSettled(Array.from({ length: Math.min(WORKSPACE_READS_AT_ONCE, snapshot.reports.length) }, async () => {
97
102
  while (next < snapshot.reports.length) {
98
103
  const index = next++
99
104
  const access = snapshot.reports[index]!
@@ -45,25 +45,45 @@ export async function putUploadPart(store: BlobStore, session: string, kind: Upl
45
45
  await store.put(partId(session, kind, id, index), bytes)
46
46
  }
47
47
 
48
- export async function readUpload(store: BlobStore, req: IncomingMessage, session: string, kind: UploadKind, maxBytes: number): Promise<Buffer> {
48
+ // Parts read at once while an upload is put together.
49
+ const PARTS_READ_AT_ONCE = 6
50
+
51
+ // The staged upload `req` names, its parts read a few at a time into the one
52
+ // buffer it fills, each as long as its place in it. Its parts are deleted
53
+ // once read, or once it fails: awaited, unless `behind` takes that cleanup to
54
+ // go on past the response (http.ts), which staging maintenance backs up.
55
+ export async function readUpload(store: BlobStore, req: IncomingMessage, session: string, kind: UploadKind, maxBytes: number,
56
+ behind?: (cleanup: Promise<void>) => void): Promise<Buffer> {
49
57
  const id = req.headers['x-upload-id']
50
58
  const count = Number(req.headers['x-upload-parts']), size = Number(req.headers['x-upload-size'])
51
59
  if (typeof id !== 'string' || !validUpload(id, count)) throw new Error('bad-upload')
52
60
  try {
53
61
  if (!Number.isSafeInteger(size) || size <= 0 || count !== Math.ceil(size / UPLOAD_CHUNK_BYTES)) throw new Error('bad-upload')
54
62
  if (size > maxBytes || size > MAX_STAGED_BYTES) throw new Error('too-large')
55
- const parts: Buffer[] = []
56
- for (let index = 0; index < count; index++) {
57
- const bytes = await store.get(partId(session, kind, id, index))
58
- const expected = Math.min(UPLOAD_CHUNK_BYTES, size - index * UPLOAD_CHUNK_BYTES)
59
- if (!bytes || bytes.length !== expected) throw new Error('bad-upload')
60
- parts.push(bytes)
63
+ // Every byte of it is written below before it is returned: each part
64
+ // must fill its place exactly, or the upload fails.
65
+ const body = Buffer.allocUnsafe(size)
66
+ let failed = false, next = 0
67
+ const readParts = async () => {
68
+ while (!failed && next < count) {
69
+ const index = next++
70
+ const bytes = await store.get(partId(session, kind, id, index))
71
+ const expected = Math.min(UPLOAD_CHUNK_BYTES, size - index * UPLOAD_CHUNK_BYTES)
72
+ if (!bytes || bytes.length !== expected) { failed = true; throw new Error('bad-upload') }
73
+ bytes.copy(body, index * UPLOAD_CHUNK_BYTES)
74
+ }
61
75
  }
62
- return Buffer.concat(parts, size)
76
+ // Each read settled before its parts are deleted.
77
+ const results = await Promise.allSettled(Array.from({ length: Math.min(PARTS_READ_AT_ONCE, count) }, readParts))
78
+ const rejected = results.find(result => result.status === 'rejected')
79
+ if (rejected) throw rejected.reason
80
+ return body
63
81
  } finally {
64
82
  // These bytes can no longer be finalized. Failed/abandoned chunk uploads
65
83
  // are also covered by automatic staging maintenance.
66
- await deleteUpload(store, session, kind, id, count).catch(err => console.warn('managed: upload cleanup failed:', err))
84
+ const cleanup = deleteUpload(store, session, kind, id, count).catch(err => console.warn('managed: upload cleanup failed:', err))
85
+ if (behind) behind(cleanup)
86
+ else await cleanup
67
87
  }
68
88
  }
69
89
 
@@ -1 +0,0 @@
1
- import{z as e}from"./chunk-S6GIXWQW.js";import{m as t,p as n,u as r}from"./chunk-JKGQ3O7O.js";import{a as i}from"./chunk-3PS4KWD2.js";import{b as a}from"./chunk-IYPY47MC.js";import{D as o,J as s,L as c,q as l}from"./chunk-SXV2T5C4.js";function u(e){let{data:t}=c(e);return t&&d(t)>0?t:null}function d(e){return(s(e)??[]).filter(t=>(Array.isArray(t)?t:[t]).some(t=>t&&typeof t==`object`&&l(t,t.source??e.source))).length}function f(e,t){let n=[],r=new Map;for(let{name:t,content:i,fallbackRepo:a}of e){if(typeof i!=`string`)continue;let e=u(i);if(!e)continue;let c=s(e),l=o(e)??o({repo:{github:a}}),f=new Set(l?[l]:[]);if(!l)for(let e of c.flat()){if(/(?:^|\/)(?:node_modules|dependencies)\//u.test(e?.file??``))continue;let t=o(e);t&&f.add(t)}for(let e of f)r.set(e,{id:e,label:e});n.push({id:t,filename:t,repoIds:[...f],analyzer:e.source,appFindings:d(e)})}let i=new Set(n.map(e=>e.id));return{merge:{bundles:[],results:[]},link:{reports:n,repositories:[...r.values()],workspaces:t.map(e=>({id:e.id,label:e.name,reports:e.reports.filter(e=>i.has(e))})).filter(e=>e.reports.length>0)}}}function p(e,t){let n=(e.reports??[]).filter(e=>e.visible&&e.repoId!=null).flatMap(e=>{let t=u(e.content??``);return t?[{id:e.id,filename:e.filename,repoIds:[e.repoId],repo:e.repoFullName,directory:e.repoDirectory,analyzer:e.analyzer,appFindings:d(t)}]:[]}),r=new Map((e.repos??[]).map(e=>[e.repoId,{id:e.repoId,label:e.fullName}]));for(let e of n)r.has(e.repoIds[0])||r.set(e.repoIds[0],{id:e.repoIds[0],label:e.repo??String(e.repoIds[0])});let a=t.results??[],o=new Set(a.map(e=>e.bundleId));return{merge:{bundles:(t.bundles??[]).filter(e=>o.has(e.id)).map(e=>({...e,size:i(e.byteSize)})),results:a},link:{repositories:[...r.values()].filter(e=>n.some(t=>t.repoIds.includes(e.id))),reports:n}}}var m=new Map([[`openai/gpt-6-astra`,`GPT-6 Astra`],[`openai/gpt-6-astra-pro`,`GPT-6 Astra Pro`],[`openai/gpt-5.6-sol`,`GPT-5.6 Sol`],[`openai/gpt-5.6-terra`,`GPT-5.6 Terra`],[`openai/gpt-5.6-luna`,`GPT-5.6 Luna`],[`anthropic/claude-fable-5.1`,`Claude Fable 5.1`],[`anthropic/claude-opus-5`,`Claude Opus 5`],[`anthropic/claude-sonnet-5`,`Claude Sonnet 5`],[`moonshotai/kimi-k3`,`Kimi K3`],[`z-ai/glm-5.3`,`GLM 5.3`],[`qwen/qwen3.8-max`,`Qwen3.8 Max`],[`deepseek/deepseek-v4.1-flash`,`DeepSeek V4.1 Flash`],[`deepseek/deepseek-v4-pro`,`DeepSeek V4 Pro`]]),h=new Map([[`openai`,`OpenAI`],[`anthropic`,`Anthropic`],[`moonshotai`,`Moonshot AI`],[`google`,`Google`],[`nvidia`,`NVIDIA`],[`qwen`,`Qwen`],[`deepseek`,`DeepSeek`],[`x-ai`,`xAI`],[`z-ai`,`Z.ai`],[`mistralai`,`Mistral AI`],[`meta`,`Meta`],[`unbiased`,`Unbiased`]]),g=new Map([[`meta-llama`,`meta`]]),_=new Map([[`gpt`,`GPT`],[`gptoss`,`GPT OSS`],[`oss`,`OSS`],[`o`,`O`],[`ai`,`AI`],[`api`,`API`],[`4o`,`4o`],[`k3`,`K3`],[`k4`,`K4`],[`qwen`,`Qwen`],[`kimi`,`Kimi`],[`mimo`,`MiMo`],[`gemini`,`Gemini`],[`claude`,`Claude`],[`fable`,`Fable`],[`opus`,`Opus`],[`sonnet`,`Sonnet`],[`astra`,`Astra`],[`terra`,`Terra`],[`luna`,`Luna`],[`codex`,`Codex`],[`deepseek`,`DeepSeek`],[`nemotron`,`Nemotron`],[`gemma`,`Gemma`]]);function v(e){let t=e.replace(/:free$/u,``).replaceAll(`_`,`-`).split(`-`).filter(Boolean),n=[];for(let e=0;e<t.length;e++){let r=t[e],i=t[e+1];if(/^\d+$/u.test(r)&&/^\d+$/u.test(i??``)){n.push(`${r}.${i}`),e++;continue}let a=_.get(r.toLowerCase());n.push(a??(/^(?:[a-z]+\d+|\d+[a-z]+)$/iu.test(r)?r.toUpperCase():r[0]?.toUpperCase()+r.slice(1)))}return n.join(` `).replace(/^GPT (\d)/u,`GPT-$1`)}function y(e){return m.get(e)??v(e.includes(`/`)?e.slice(e.indexOf(`/`)+1):e)}function b(e){let t=e.includes(`/`)?e.split(`/`)[0].replace(/^~/u,``):`other`,n=g.get(t)??t;return{key:n,name:h.get(n)??(n===`other`?`Other`:v(n))}}function x(e){return e===`xhigh`?`Extra high`:e?e[0].toUpperCase()+e.slice(1):`Unavailable`}function S(e){return e.efforts.includes(`max`)?`max`:e.efforts.at(-1)??null}async function C(e,t=fetch){let n=await t(`/api/admin/scan/models`,{credentials:`same-origin`,headers:{accept:`application/json`},signal:e});if(!n.ok)throw Error(`HTTP ${n.status}`);return w(await n.json())}function w(e){if(!Array.isArray(e?.models))throw Error(`No model catalogue returned`);let t=e.models.filter(e=>typeof e?.id==`string`&&e.id.length>0).map(e=>({id:e.id,efforts:Array.isArray(e.efforts)?[...new Set(e.efforts.filter(e=>typeof e==`string`&&e.length>0))]:[]}));if(t.length===0)throw Error(`No models available`);return{models:t,defaultModel:t.find(t=>t.id===e.defaultModel)?.id??t[0].id}}function T(e,t=[]){let n=new Map,r=e.flatMap(e=>{let r=t.filter(t=>t.bundles?.includes(e.integrity));return(r.length>0?r.map(e=>({id:e.id,label:e.name})):[{id:`unattached`,label:`Unattached`}]).map(t=>(n.set(t.id,t),{id:e.integrity,integrity:e.integrity,filename:e.name,repoId:t.id,repo:t.label,files:null,reasons:[],size:`—`}))});return{repositories:[...n.values()],bundles:r,reports:[],scans:[]}}function E(o,s){if(s.error)throw Error(s.error);let c=n(s),l=s.kind===`stasis`?s.bundle?.formats:null,u=r(s),d=[...t(s)].filter(([,e])=>e!=null).map(([e,t])=>({path:e,format:l?.get(e)??null,bytes:t,lines:u.get(e)??0,size:i(t),module:a(e,{packageDir:c?.get(e)})})),f=[{id:`all`,label:`All`,filePaths:null},...[...e(s,d.map(e=>e.path))].map(([e,t])=>({id:`reason:${e}`,label:e,filePaths:[...t]}))];return{...o,files:d,reasons:f,size:i(s.size)}}export{T as a,E as b,f as c,p as d,y as e,b as f,x as g,S as h,C as i,w as j};