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

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 (38) hide show
  1. package/common/managed/finding-detail.d.ts +2 -0
  2. package/common/managed/finding-detail.js +32 -0
  3. package/common/managed/github-agents.ts +6 -3
  4. package/common/managed/routes.js +14 -2
  5. package/out/activity.js +4 -4
  6. package/out/{chunk-ULIF7A7N.js → chunk-2WDZPXVL.js} +1 -1
  7. package/out/chunk-4XMP7YVH.js +36 -0
  8. package/out/chunk-6S2U2UYG.js +232 -0
  9. package/out/{chunk-IO7JTZ6O.js → chunk-75YLHBCM.js} +1 -1
  10. package/out/chunk-FC65TPUB.js +1 -0
  11. package/out/{chunk-O6M574JI.js → chunk-FKBXNUW4.js} +1 -1
  12. package/out/{chunk-PBSOL2SL.js → chunk-HTBYHSPG.js} +1 -1
  13. package/out/{chunk-SZD5CW5R.js → chunk-UK7RGJT6.js} +2 -2
  14. package/out/client-managed-import.js +1 -1
  15. package/out/client-managed.js +2 -2
  16. package/out/compare.js +14 -14
  17. package/out/dialogs.js +34 -30
  18. package/out/graph.js +1 -1
  19. package/out/index.html +6 -6
  20. package/out/scan.js +2 -2
  21. package/out/view.css +1 -1
  22. package/out/view.js +15 -15
  23. package/package.json +4 -1
  24. package/server-managed/agent-merges.ts +31 -136
  25. package/server-managed/app-agents.ts +4 -0
  26. package/server-managed/commit-coauthors.ts +46 -24
  27. package/server-managed/http.ts +115 -37
  28. package/server-managed/npm-kept.ts +8 -0
  29. package/server-managed/public-workspace.ts +2 -2
  30. package/server-managed/repository-agents.ts +189 -57
  31. package/server-managed/repository-history.ts +51 -13
  32. package/server-managed/repository-pulls.ts +156 -0
  33. package/server-managed/repository-pulse.ts +3 -2
  34. package/server-managed/repository-squashes.ts +29 -6
  35. package/server-managed/team-reports.ts +33 -18
  36. package/out/chunk-BC2UCLFJ.js +0 -1
  37. package/out/chunk-C2AVODGZ.js +0 -36
  38. package/out/chunk-JEAUAJVF.js +0 -232
@@ -135,8 +135,9 @@ import { mergeLinkGroups } from './link-reports.ts'
135
135
  import { backfillFindingIds, reportEntries } from '@preventive/report'
136
136
  import type { IncomingMessage, ServerResponse } from 'node:http'
137
137
  import { pipeline } from 'node:stream/promises'
138
- import { type KeptStore, REPOSITORY_READ_KEPT_MS, codeHeadStore, codeTreeStore, historyChangeStore, historyPageStore, historySquashStore, historyTreeStore, lockfileReadStore,
139
- npmDeltaStore, npmPublishTimeStore, npmVersionStore, ownKeptPrunes, pullDependencyStore, repositoryReadStore, reviewBaselineStore, riskLineStore, scanReadStore } from './npm-kept.ts'
138
+ import { type KeptStore, REPOSITORY_READ_KEPT_MS, codeHeadStore, codeTreeStore, commitPullStore, historyChangeStore, historyPageStore, historySquashStore, historyTreeStore,
139
+ lockfileReadStore,
140
+ npmDeltaStore, npmPublishTimeStore, npmVersionStore, ownKeptPrunes, pullDependencyStore, pullListStore, repositoryReadStore, reviewBaselineStore, riskLineStore, scanReadStore } from './npm-kept.ts'
140
141
  import { plainObject } from './npm-manifest.ts'
141
142
  import { type NpmDelta, keptNpmDeltas, npmDelta } from './npm-delta.ts'
142
143
  import { ADVISORIES_TIMEOUT_MS, fetchBundleAdvisories } from './bundle-advisories.ts'
@@ -190,7 +191,7 @@ import { isExactVersion } from '@preventive/upstream/semver.js'
190
191
  import { VIEW_AS_PATH, VIEW_ONLY_ERROR, clearViewCookie, endViewSession, runViewing, startViewSession, viewToken } from './view-as.ts'
191
192
  import type { ActivityContext, ActivityInput } from './activity.ts'
192
193
  import { acceptsReportMetadata } from './report-response.ts'
193
- import { TeamReportsError, loadTeamAnnotations, loadTeamReportsResponse, loadTeamTreesResponse, recheckTeam, teamFindingIds, teamSnapshot, teamSourcePaths, teamWorkspaceFindingIds } from './team-reports.ts'
194
+ import { TeamReportsError, type TeamResponsePart, loadTeamAnnotations, loadTeamReportsResponse, recheckTeam, teamFindingIds, teamResponsePart, teamSnapshot, teamSourcePaths, teamWorkspaceFindingIds } from './team-reports.ts'
194
195
  import { FINDING_CATALOG_PAGE_BYTES, FINDING_CATALOG_PAGE_COUNT, MAX_REPORT_QUERY_BYTES, MAX_REPORT_QUERY_COUNT } from './report-query.ts'
195
196
  import { lookupFixes, storedFixUrls } from './github-pulls.ts'
196
197
  import { lookupIssueLinks } from './github-issue-links.ts'
@@ -214,11 +215,11 @@ import { NpmPackageError, type NpmReader, type NpmVersionDocument, type NpmVersi
214
215
  import { loadNpmFiles, loadNpmPackageBody, loadNpmTarball } from './npm-loads.ts'
215
216
  import { type RepositoryCodeKept, loadRepositoryCodeBody, repositoryCodeFits, repositoryCodeKept } from './repository-code.ts'
216
217
  import { type ChangesStore, type CommitChanges, type CommitTree, TREES_READ, changesAsked, changesWithin, directoryFiles, repositoryChanges } from './repository-changes.ts'
217
- import { COAUTHORS_READ } from './commit-coauthors.ts'
218
+ import { COAUTHORS_READ, coauthorsOf } from './commit-coauthors.ts'
218
219
  import { type AllHeads, type HistoryCommit, type HistoryPages, MAX_HISTORY_PAGE, historyFrom, historyStarts, openPullHeads,
219
220
  repositoryAllHeads, repositoryAllHistory, repositoryHistory, repositoryRefTips } from './repository-history.ts'
220
- import { type SquashedPull, type Squashes, squashedPulls } from './repository-squashes.ts'
221
- import { AGENTS_READ_VERSION, COMMITS_CURSOR, repositoryAgents } from './repository-agents.ts'
221
+ import { type KnownMerges, type SquashedPull, type Squashes, squashedPulls } from './repository-squashes.ts'
222
+ import { AGENTS_READ_VERSION, COMMITS_CURSOR, type CommitPulls, repositoryAgents } from './repository-agents.ts'
222
223
  import { ISSUES_CURSOR, ISSUES_READ_VERSION, repositoryIssues, repositoryItem } from './repository-issues.ts'
223
224
  import { type GithubAsked, type ListedLockfile, type LockfileRead, type LockfileStore, MAX_LOCKED, PULL_DEPENDENCIES_READ, type PullCommits, type PullDependencies,
224
225
  type PullDependencyStore, eachOf, lockfileFormat, pullDependenciesAt } from './pull-dependencies.ts'
@@ -232,6 +233,8 @@ import { commentReviews } from './pull-comment-reviews.ts'
232
233
  import { PULSE_WEEKS, PULSE_YEAR_VERSION, type PulseStats, StatsPending, commitStats, defaultHead, periodStats, pulseSince, repositoryActivity, repositoryLines,
233
234
  repositoryStats, withLines, yearCommits } from './repository-pulse.ts'
234
235
  import { type AgentMerges, agentCommits, agentMerges, directCommits, sameMerges } from './agent-merges.ts'
236
+ import { PULLS_READ_VERSION, type PullList, pullsReadTo, readPulls } from './repository-pulls.ts'
237
+ import { AGENT_MARKS_READ } from './app-agents.ts'
235
238
  import { releaseCommits } from './release-commits.ts'
236
239
  import { computeBundleDiff } from '../common/bundle-compare-diff.js'
237
240
  import type { ReviewChange, ReviewContext, ReviewInput, ReviewedTarballs } from './dependency-reviews.ts'
@@ -1882,7 +1885,7 @@ async function handleRepositoryHistory(res: ServerResponse, deps: ManagedHttpDep
1882
1885
  const listed = (body as { commits: HistoryCommit[] }).commits
1883
1886
  const none = { pulls: [] as SquashedPull[], pending: false }
1884
1887
  const [squashed, openPulls] = token && path === '' ? await Promise.all([
1885
- squashedPulls(repo, token, listed, historySquashes(deps, repoId), SQUASH_WAIT_MS).then(read => {
1888
+ pulseMerges(deps, repoId).catch(quiet(null)).then(known => squashedPulls(repo, token, listed, historySquashes(deps, repoId), SQUASH_WAIT_MS, globalThis.fetch, known)).then(read => {
1886
1889
  if (read.behind) {
1887
1890
  const behind = read.behind.catch(quiet(null))
1888
1891
  deps.track(behind)
@@ -1897,10 +1900,11 @@ async function handleRepositoryHistory(res: ServerResponse, deps: ManagedHttpDep
1897
1900
  const current = await authorize()
1898
1901
  if (!current) return
1899
1902
  if (!sameRepository(current.repo, access.repo)) { sendJson(res, 409, { error: 'repository-changed' }); return }
1900
- // Each commit as the tab draws it, its co-authors with it: what Pulse
1901
- // counts of it (its lines, its author's time) not sent.
1902
- const shown = ({ sha, parents, subject, authorName, authorLogin, authorId, coauthors, committedAt }: HistoryCommit) =>
1903
- ({ sha, parents, subject, authorName, authorLogin, authorId: authorId ?? null, coauthors: coauthors ?? [], committedAt })
1903
+ // Each commit as the tab draws it, its co-authors with it, told by the
1904
+ // agents the app knows now (coauthorsOf): what Pulse counts of it (its
1905
+ // lines, its author's time) and the addresses kept not sent.
1906
+ const shown = (commit: HistoryCommit) => ({ sha: commit.sha, parents: commit.parents, subject: commit.subject, authorName: commit.authorName,
1907
+ authorLogin: commit.authorLogin, authorId: commit.authorId ?? null, coauthors: coauthorsOf(commit), committedAt: commit.committedAt })
1904
1908
  sendJson(res, 200, await withAppUsers(deps.db, { ...body, commits: listed.map(shown),
1905
1909
  ...pulls.length > 0 ? { pulls: pulls.map(pull => ({ ...pull, commits: pull.commits.map(shown) })) } : {}, ...squashed.pending ? { pullsPending: true } : {},
1906
1910
  ...openPulls?.length ? { openPulls } : {} }))
@@ -2074,14 +2078,16 @@ async function keptAnswer<T extends object>(deps: ManagedHttpDeps, key: string,
2074
2078
  // telling each commit's co-authors (kept since they are read), and with
2075
2079
  // `graphql`, only those read with GraphQL, telling its lines and the accounts
2076
2080
  // its co-authors' addresses are (a page GitHub's REST API listed tells
2077
- // neither); the rest read again.
2078
- function historyPages(deps: ManagedHttpDeps, repoId: number, graphql = false): HistoryPages {
2081
+ // neither), and with `marks`, only those telling the marks agents left on
2082
+ // its commits by the rules they are read by now (app-agents.ts
2083
+ // AGENT_MARKS_READ), as the Agents tab reads them; the rest read again.
2084
+ function historyPages(deps: ManagedHttpDeps, repoId: number, graphql = false, marks = false): HistoryPages {
2079
2085
  const store = historyPageStore<HistoryCommit[]>(deps.db, deps.config.debug)
2080
2086
  const keyOf = (top: string) => `${repoId}\0${COAUTHORS_READ}\0${top}`
2081
2087
  const told = (tops: string[], kept: Map<string, { value: HistoryCommit[] }>) => new Map(tops.flatMap(top => {
2082
2088
  const page = kept.get(keyOf(top))?.value
2083
- return Array.isArray(page) && page.every(commit => Array.isArray(commit.coauthors)) && (!graphql || page.every(commit => commit.additions != null))
2084
- ? [[top, page] as const] : []
2089
+ return Array.isArray(page) && page.every(commit => Array.isArray(commit.named)) && (!graphql || page.every(commit => commit.additions != null))
2090
+ && (!marks || page.every(commit => commit.marked === AGENT_MARKS_READ)) ? [[top, page] as const] : []
2085
2091
  }))
2086
2092
  return {
2087
2093
  read: async tops => told(tops, await store.readMany(tops.map(keyOf))),
@@ -2093,6 +2099,73 @@ function historyPages(deps: ManagedHttpDeps, repoId: number, graphql = false): H
2093
2099
  }
2094
2100
  }
2095
2101
 
2102
+ // Whether a pull request brought each of repository `repoId`'s commits, as
2103
+ // the Agents tab looks it up of those an agent marked (repository-agents.ts
2104
+ // pulledCommits), kept by the commit.
2105
+ function commitPulls(deps: ManagedHttpDeps, repoId: number): CommitPulls {
2106
+ const store = commitPullStore<{ pulled: boolean }>(deps.db, deps.config.debug)
2107
+ const keyOf = (sha: string) => `${repoId}\0${sha}`
2108
+ return {
2109
+ async read(shas) {
2110
+ const kept = await store.readMany(shas.map(keyOf))
2111
+ return new Map(shas.flatMap(sha => {
2112
+ const pulled = kept.get(keyOf(sha))?.value?.pulled
2113
+ return typeof pulled === 'boolean' ? [[sha, pulled] as const] : []
2114
+ }))
2115
+ },
2116
+ write: (sha, pulled) => store.write(keyOf(sha), { pulled }, Date.now()),
2117
+ }
2118
+ }
2119
+
2120
+ // How long repository `repoId`'s pull requests kept (keptPulls) are taken as
2121
+ // they are before those updated since are read: less than its readers keep
2122
+ // their answers (keptAnswer), that one read of them serves both.
2123
+ const PULLS_FRESH_MS = 30_000
2124
+ // Each repository's reads of its pull requests, by its id: one at a time,
2125
+ // each after those asked before it.
2126
+ const pullsReads = new Map<number, Promise<unknown>>()
2127
+
2128
+ // Repository `repoId`'s pull requests, as kept (repository-pulls.ts
2129
+ // readPulls), read on where they were read PULLS_FRESH_MS ago or longer, or
2130
+ // not as far back as `need`: as far back as it, for the Pulse; for the
2131
+ // Agents tab (null), as far back as they were read, those updated since read
2132
+ // (a read not finished, on), else their first page. One read of a
2133
+ // repository's at a time, the rest after it, from what it kept.
2134
+ function keptPulls(deps: ManagedHttpDeps, repoId: number, fullName: string, token: string, need: number | null): Promise<PullList> {
2135
+ const store = pullListStore<PullList>(deps.db, deps.config.debug)
2136
+ const key = `${repoId}\0${PULLS_READ_VERSION}\0${AGENTS_READ_VERSION}\0${PULSE_YEAR_VERSION}`
2137
+ const reading = (pullsReads.get(repoId) ?? Promise.resolve()).catch(() => {}).then(async () => {
2138
+ const kept = await store.read(key)
2139
+ const list = kept?.value ?? null
2140
+ if (list && Date.now() - kept!.at < PULLS_FRESH_MS && (need === null || pullsReadTo(list) <= need)) return list
2141
+ const since = pulseSince(52, Date.now())
2142
+ const read = await readPulls(fullName, token, since, list, globalThis.fetch,
2143
+ need ?? (list ? list.next ? since : list.reach ?? since : Date.now()))
2144
+ await store.write(key, read, Date.now())
2145
+ return read
2146
+ })
2147
+ pullsReads.set(repoId, reading)
2148
+ void reading.catch(() => {}).finally(() => { if (pullsReads.get(repoId) === reading) pullsReads.delete(repoId) })
2149
+ return reading
2150
+ }
2151
+
2152
+ // What the Pulse keeps of repository `repoId`'s pull requests merged, as
2153
+ // it tells by them whose its year's commits are (pulseCommits, agentMerges).
2154
+ const pulseMergesKey = (repoId: number) => `${repoId}\0pulse-merges\0${PULSE_YEAR_VERSION}`
2155
+
2156
+ // The pull requests merged as the Pulse last read them through (a read not
2157
+ // finished, none), for the Commits tab to tell those squashed by
2158
+ // (repository-squashes.ts KnownMerges): each's merge commit and its number
2159
+ // (none of one read before numbers were), every one merged from as far
2160
+ // back as the read reaches (the year's start, or as far as a period needed)
2161
+ // till the newest read among them; null where none is kept.
2162
+ async function pulseMerges(deps: ManagedHttpDeps, repoId: number): Promise<KnownMerges | null> {
2163
+ const kept = (await repositoryReadStore(deps.db, deps.config.debug).read(pulseMergesKey(repoId)))?.value as AgentMerges | undefined
2164
+ if (!kept || kept.next || kept.through === null || !Array.isArray(kept.pulls)) return null
2165
+ return { numbers: new Map(kept.pulls.map(([sha, , number]) => [sha, number ?? null])), from: Math.max(kept.reach ?? Number.NEGATIVE_INFINITY, pulseSince(52, Date.now())),
2166
+ to: kept.through }
2167
+ }
2168
+
2096
2169
  // The pull requests squashed into repository `repoId`'s commits, kept by
2097
2170
  // each commit and the rules their commits' co-authors were read by
2098
2171
  // (COAUTHORS_READ), null for one none was (repository-squashes.ts
@@ -2106,7 +2179,7 @@ function historySquashes(deps: ManagedHttpDeps, repoId: number): Squashes {
2106
2179
  const kept = await store.readMany(shas.map(keyOf))
2107
2180
  return new Map(shas.flatMap(sha => {
2108
2181
  const entry = kept.get(keyOf(sha))
2109
- return entry && (entry.value === null || (Array.isArray(entry.value.commits) && entry.value.commits.every(commit => Array.isArray(commit.coauthors))))
2182
+ return entry && (entry.value === null || (Array.isArray(entry.value.commits) && entry.value.commits.every(commit => Array.isArray(commit.named))))
2110
2183
  ? [[sha, entry.value] as const] : []
2111
2184
  }))
2112
2185
  },
@@ -2213,9 +2286,10 @@ async function handleRepositoryIssues(res: ServerResponse, deps: ManagedHttpDeps
2213
2286
 
2214
2287
  // GET /api/admin/repositories/agents: what the code viewer's Agents tab
2215
2288
  // reconstructs agents' sessions from (repository-agents.ts): a page
2216
- // of pull requests on from the cursor `pulls` names, and of the default
2217
- // branch's commits on from `commits`, the first page, naming neither, with
2218
- // issues and branches. They are the whole repository's (readWholeRepository).
2289
+ // of pull requests below the one the cursor `pulls` names, of those kept for
2290
+ // the Pulse too (keptPulls), and of the default branch's commits on from
2291
+ // `commits`, the first page, naming neither, with issues and branches. They
2292
+ // are the whole repository's (readWholeRepository).
2219
2293
  async function handleRepositoryAgents(res: ServerResponse, deps: ManagedHttpDeps, cookie: string | undefined, query: URLSearchParams): Promise<void> {
2220
2294
  const repoId = Number(query.get('repoId'))
2221
2295
  const cursors = { pulls: query.get('pulls'), commits: query.get('commits') }
@@ -2223,7 +2297,8 @@ async function handleRepositoryAgents(res: ServerResponse, deps: ManagedHttpDeps
2223
2297
  || (cursors.commits !== null && !COMMITS_CURSOR.test(cursors.commits))) {
2224
2298
  sendJson(res, 400, { error: 'bad-request' }); return
2225
2299
  }
2226
- await readWholeRepository(res, deps, cookie, repoId, (fullName, token) => repositoryAgents(fullName, token, cursors),
2300
+ await readWholeRepository(res, deps, cookie, repoId, (fullName, token) => repositoryAgents(fullName, token, cursors, historyPages(deps, repoId, true, true),
2301
+ commitPulls(deps, repoId), () => keptPulls(deps, repoId, fullName, token, null)),
2227
2302
  `agents\0${AGENTS_READ_VERSION}\0${cursors.pulls ?? ''}\0${cursors.commits ?? ''}`)
2228
2303
  }
2229
2304
 
@@ -2656,9 +2731,10 @@ async function handleRepositoryPulse(res: ServerResponse, deps: ManagedHttpDeps,
2656
2731
  // its commits as the Commits tab keeps them, with their lines
2657
2732
  // (yearCommits): kept by the head it was read from (repositoryReadStore),
2658
2733
  // that head kept as an issue's page is; its commits merged from an agent's
2659
- // pull request told by those merged (agentMerges), kept as read, the rest
2660
- // read on by the next answer as the year's commits are, and read again as
2661
- // an answer kept is, a label changing whose a pull request is without
2734
+ // pull request told by those merged (agentMerges), of the pull requests
2735
+ // kept for the Agents tab too (keptPulls), what they tell kept as read, the
2736
+ // rest read on by the next answer as the year's commits are, and read again
2737
+ // as an answer kept is, a label changing whose a pull request is without
2662
2738
  // moving the head, the year counted again where what they bring changed.
2663
2739
  // Read no further back than the period asked (`weeks`) needs, its weeks and
2664
2740
  // as many before them, the rest read only as a longer one asks, its count
@@ -2718,25 +2794,26 @@ async function pulseCommits(deps: ManagedHttpDeps, repoId: number, fullName: str
2718
2794
  const store = repositoryReadStore(deps.db, deps.config.debug)
2719
2795
  const key = `${repoId}\0pulse-year\0${PULSE_YEAR_VERSION}\0${head.value.sha}\0${first}`
2720
2796
  const lastKey = `${repoId}\0pulse-year-last`
2721
- const mergesKey = `${repoId}\0pulse-merges\0${PULSE_YEAR_VERSION}`
2797
+ const mergesKey = pulseMergesKey(repoId)
2722
2798
  const last = async () => {
2723
2799
  const kept = (await store.read(lastKey))?.value as { first: number; stats: PulseStats } | undefined
2724
2800
  return kept && first - kept.first <= PULSE_LAST_MS ? kept.stats : null
2725
2801
  }
2726
2802
  const sha = head.value.sha
2727
2803
  // What tells whose the year's commits are, kept as read: the agents' pull
2728
- // requests merged (agentMerges), and the commits that release the
2804
+ // requests merged (agentMerges), as the pull requests kept tell, read as
2805
+ // far back as `whole` (keptPulls), and the commits that release the
2729
2806
  // repository (`releases`, release-commits.ts releaseCommits).
2730
2807
  type Attribution = AgentMerges & { releases?: string[] }
2731
2808
  const keptAttribution = async () => ((await store.read(mergesKey))?.value as Attribution | undefined) ?? null
2732
- const attribution = async (kept: Attribution | null, whole: number): Promise<Attribution> => {
2733
- const [merges, releases] = await Promise.all([agentMerges(fullName, token, first, kept, globalThis.fetch, whole),
2809
+ const attribution = async (whole: number): Promise<Attribution> => {
2810
+ const [pulls, releases] = await Promise.all([keptPulls(deps, repoId, fullName, token, whole),
2734
2811
  releaseCommits(fullName, token, first, historyChanges(deps, repoId))])
2735
- return { ...merges, releases }
2812
+ return { ...agentMerges(pulls, first), releases }
2736
2813
  }
2737
- // Since when every pull request merged is read (agentMerges `reach`, or
2738
- // a read not finished's `to`): all of them, -∞; none yet, ∞.
2739
- const readTo = (read: Attribution) => read.next ? read.next.to ?? Number.POSITIVE_INFINITY : read.reach ?? Number.NEGATIVE_INFINITY
2814
+ // Since when every pull request merged is read (repository-pulls.ts
2815
+ // pullsReadTo): all of them, -∞; none yet, ∞.
2816
+ const readTo = (read: Attribution) => pullsReadTo(read)
2740
2817
  // The year counted from its `commits` as `read` tells whose they are and
2741
2818
  // which were pushed directly, the branch followed through those it was
2742
2819
  // read on past (`passed`, yearCommits) too, whole from when both the
@@ -2761,7 +2838,7 @@ async function pulseCommits(deps: ManagedHttpDeps, repoId: number, fullName: str
2761
2838
  const whole = stats.from ?? first
2762
2839
  await keptAnswer(deps, mergesKey, async () => {
2763
2840
  const kept = await keptAttribution()
2764
- const read = await attribution(kept, whole)
2841
+ const read = await attribution(whole)
2765
2842
  const same = kept && sameMerges(read, kept) && read.releases!.join() === (kept.releases ?? []).join() && readTo(kept) <= readTo(read)
2766
2843
  if (readTo(read) >= whole || same) return read
2767
2844
  const year = await yearCommits(fullName, token, sha, first, historyPages(deps, repoId, true), globalThis.fetch, whole)
@@ -2779,7 +2856,7 @@ async function pulseCommits(deps: ManagedHttpDeps, repoId: number, fullName: str
2779
2856
  }
2780
2857
  if (!held || !('tooMany' in held)) {
2781
2858
  const [year, read] = await Promise.all([yearCommits(fullName, token, sha, first, historyPages(deps, repoId, true), globalThis.fetch, need),
2782
- keptAttribution().then(kept => attribution(kept, need))])
2859
+ attribution(need)])
2783
2860
  await store.write(mergesKey, read, Date.now())
2784
2861
  if (year.complete) {
2785
2862
  if (readTo(read) >= need) return { stats: await last(), reading: null, pulls: read.next?.read ?? 0, pending: false, kept: false }
@@ -3714,11 +3791,12 @@ async function handleQueryReports(req: IncomingMessage, res: ServerResponse, dep
3714
3791
 
3715
3792
  // The server selects the complete published workspace for classification.
3716
3793
  // Privileged hidden-report previews are independently scoped and bounded.
3717
- // Its reports' file trees are asked for apart, by its graph or Files alone.
3794
+ // Its reports' file trees are asked for apart, by its graph or Files alone,
3795
+ // and its board's reports without their findings' detail (`?lite=1`).
3718
3796
  async function handleTeamReports(res: ServerResponse, deps: ManagedHttpDeps, session: ManagedSession, teamId: string, reportId: string | null,
3719
- part: 'reports' | 'trees' = 'reports'): Promise<void> {
3797
+ part: TeamResponsePart = 'reports'): Promise<void> {
3720
3798
  const snapshot = await teamSnapshot(deps.db, session.id, teamId, reportId)
3721
- const body = await (part === 'trees' ? loadTeamTreesResponse : loadTeamReportsResponse)(deps.db, deps.reportStore, snapshot)
3799
+ const body = await loadTeamReportsResponse(deps.db, deps.reportStore, snapshot, part)
3722
3800
  await recheckTeam(deps.db, session.id, snapshot)
3723
3801
  res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store', 'x-content-type-options': 'nosniff' })
3724
3802
  writeResponse(res, body)
@@ -5092,7 +5170,7 @@ export function createManagedRequestHandler(deps: ManagedHttpDeps): Handler {
5092
5170
  const teamReports = /^\/api\/teams\/([^/]+)\/(reports|trees)$/u.exec(path)
5093
5171
  if (teamReports) {
5094
5172
  if (method !== 'GET') { send405(res, 'GET'); return }
5095
- await handleTeamReports(res, deps, workspaceSession!.session, teamReports[1]!, url.searchParams.get('reportId'), teamReports[2] as 'reports' | 'trees'); return
5173
+ await handleTeamReports(res, deps, workspaceSession!.session, teamReports[1]!, url.searchParams.get('reportId'), teamResponsePart(teamReports[2]!, url)); return
5096
5174
  }
5097
5175
  if (path === REPORT_QUERY_PATH) {
5098
5176
  if (method !== 'POST') { send405(res, 'POST'); return }
@@ -161,6 +161,14 @@ export const historyPageStore = <T>(db: UpstreamCacheStore, debug = false): Kept
161
161
  // The pull requests squashed into a repository's commits, by the commit
162
162
  // (repository-squashes.ts squashedPulls): kept as its history's pages are.
163
163
  export const historySquashStore = <T>(db: UpstreamCacheStore, debug = false): KeptStore<T> => keptStore<T>(db, 'repository/squashes', HISTORY_PAGE_KEPT_MS, debug)
164
+ // Whether a pull request brought a commit, by its repository and commit, as
165
+ // the Agents tab looks it up of those an agent marked (repository-agents.ts
166
+ // pulledCommits), kept as the history is.
167
+ export const commitPullStore = <T>(db: UpstreamCacheStore, debug = false): KeptStore<T> => keptStore<T>(db, 'repository/commit-pulls', HISTORY_PAGE_KEPT_MS, debug)
168
+ // A repository's pull requests, the most recently updated first, as the
169
+ // Pulse and the Agents tab read them (repository-pulls.ts readPulls), by its
170
+ // id: kept as its history's pages are, those updated since read on.
171
+ export const pullListStore = <T>(db: UpstreamCacheStore, debug = false): KeptStore<T> => keptStore<T>(db, 'repository/pulls', HISTORY_PAGE_KEPT_MS, debug)
164
172
  // The files each of a repository's commits changes (repository-changes.ts
165
173
  // repositoryChanges), by the commit, which never change: kept as its
166
174
  // history's pages are.
@@ -7,7 +7,7 @@ import type { OpenedBlob } from './blob-store.ts'
7
7
  import type { WorkspaceShareSnapshot } from './workspace-shares.ts'
8
8
  import { sendJson, sendLines, writeResponse } from './http-response.ts'
9
9
  import { hashToken } from './crypto.ts'
10
- import { TeamReportsError, loadTeamAnnotations, loadTeamReportsResponse, loadTeamTreesResponse, teamReportVisibility } from './team-reports.ts'
10
+ import { TeamReportsError, loadTeamAnnotations, loadTeamReportsResponse, teamReportVisibility, teamResponsePart } from './team-reports.ts'
11
11
  import { triageWireEntry } from './triage-response.ts'
12
12
  import { MAX_PACKAGE_INVENTORY_BYTES } from './bundle-cache.ts'
13
13
  import { backfillBundleSummaries, bundleSummaries } from './bundle-catalog.ts'
@@ -84,7 +84,7 @@ export async function handlePublicWorkspace(req: IncomingMessage, res: ServerRes
84
84
  await send(await loadTeamAnnotations(deps.db, deps.reportStore, snapshot, url.searchParams.get('reportId'))); return
85
85
  }
86
86
  if (teamRoute[2] === 'shared') { await sendTeam(); return }
87
- const body = await (teamRoute[2] === 'trees' ? loadTeamTreesResponse : loadTeamReportsResponse)(deps.db, deps.reportStore, snapshot)
87
+ const body = await loadTeamReportsResponse(deps.db, deps.reportStore, snapshot, teamResponsePart(teamRoute[2]!, url))
88
88
  await recheck()
89
89
  res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'private, no-store', 'referrer-policy': 'no-referrer' })
90
90
  writeResponse(res, body); return