@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
@@ -0,0 +1,2 @@
1
+ export const FINDING_DETAIL_FIELDS: readonly string[]
2
+ export function liteReportData(data: unknown): unknown
@@ -0,0 +1,32 @@
1
+ import { findingTitle } from '@preventive/report'
2
+
3
+ // A finding's detail: what its popup, the views besides the board and an
4
+ // export read, and the board does not. Its narrative and evidence, the hash
5
+ // its Code button finds a bundle by, and the export it names. A team's
6
+ // reports are first sent without it (server-managed/team-reports.ts), the
7
+ // board drawn from them, and their findings filled in from the whole reports
8
+ // (ui/view/managed-details.js).
9
+ export const FINDING_DETAIL_FIELDS = Object.freeze(['description', 'evidence', 'impact', 'reproduction', 'recommendation',
10
+ 'confidenceReason', 'revalidateVerdict', 'revalidateRecommendation', 'fileHash', 'exportName', 'methodName'])
11
+ const DETAIL = new Set(FINDING_DETAIL_FIELDS)
12
+ // More of a name than a board card's three lines show.
13
+ const NAME_CHARS = 200
14
+
15
+ // A finding without its detail. One without a title of its own is named by its
16
+ // description's first line (findingTitle), which it keeps as its description,
17
+ // as much of it as a card shows.
18
+ function liteFinding(finding) {
19
+ if (!finding || typeof finding !== 'object' || Array.isArray(finding)) return finding
20
+ const lite = Object.fromEntries(Object.entries(finding).filter(([key]) => !DETAIL.has(key)))
21
+ const titled = typeof finding.title === 'string' && finding.title.trim() !== ''
22
+ const title = titled ? '' : findingTitle(finding)
23
+ if (title) lite.description = title.length > NAME_CHARS ? `${title.slice(0, NAME_CHARS - 1)}…` : title
24
+ return lite
25
+ }
26
+
27
+ // A report's data, its findings without their detail.
28
+ export function liteReportData(data) {
29
+ const key = Array.isArray(data?.findings) ? 'findings' : Array.isArray(data?.groups) ? 'groups' : null
30
+ if (!key) return data
31
+ return { ...data, [key]: data[key].map(entry => Array.isArray(entry) ? entry.map(liteFinding) : liteFinding(entry)) }
32
+ }
@@ -20,7 +20,9 @@
20
20
  // in its address. GitHub's Agentic Workflows (gh-aw) post, and approve or
21
21
  // review pull requests, as GitHub Actions' bot, `github-actions` (41898282,
22
22
  // `[bot]` to the REST API), its mark the GitHub Actions App's avatar; other
23
- // workflows the bot runs are as much its own. Neo, its GitHub App (3115942),
23
+ // workflows the bot runs are as much its own, and the commits workflows make
24
+ // as the names they are commonly given, `github-actions` and `GitHub Action`,
25
+ // GitHub naming no account for their addresses. Neo, its GitHub App (3115942),
24
26
  // as its account (274065190) and its App's bot (268952163), each known by
25
27
  // its id alone, whatever GitHub names it, and drawn by its App's avatar,
26
28
  // wherever either or its fingerprint names it. Cursor's agents commit as whoever ran them, with a
@@ -51,7 +53,8 @@ export const GITHUB_AGENTS = {
51
53
  { id: 175728472, logins: ['Copilot', 'copilot-pull-request-reviewer', 'copilot-pull-request-reviewer[bot]'] }], names: ['Copilot'], prefix: 'copilot/' },
52
54
  dependabot: { name: 'Dependabot', avatar: 'app-29110', accounts: [{ id: 49699333, logins: ['dependabot', 'dependabot[bot]'] }], names: ['dependabot[bot]'],
53
55
  prefix: 'dependabot/' },
54
- actions: { name: 'GitHub Actions', avatar: 'app-15368', accounts: [{ id: 41898282, logins: ['github-actions', 'github-actions[bot]'] }], names: ['github-actions[bot]'] },
56
+ actions: { name: 'GitHub Actions', avatar: 'app-15368', accounts: [{ id: 41898282, logins: ['github-actions', 'github-actions[bot]'] }],
57
+ names: ['github-actions[bot]', 'github-actions', 'GitHub Action'] },
55
58
  cursor: { name: 'Cursor', accounts: [], names: ['Cursor Agent'], emails: ['cursoragent@cursor.com'] },
56
59
  } as const satisfies Record<string, GithubAgent>
57
60
  export type GithubAgentId = keyof typeof GITHUB_AGENTS
@@ -99,7 +102,7 @@ export function oneAgent(login: string, ids: Iterable<number>): boolean {
99
102
  // The accounts agents work as, a person's to GitHub, as the app is told of
100
103
  // them, each by its id and logins: the pull requests they open and the
101
104
  // commits they push to a branch directly are agents' (the Pulse's,
102
- // server-managed/agent-merges.ts agentMerges, repository-pulse.ts
105
+ // server-managed/repository-pulls.ts readPulls, repository-pulse.ts
103
106
  // commitStats), whatever their branches and labels, the rest of what they do
104
107
  // theirs. Christopher Turing's.
105
108
  export const AGENT_OPERATED = [{ id: 188797125, logins: ['ChristopherTuring'] }] as const satisfies readonly GithubAgentAccount[]
@@ -146,7 +146,8 @@ function parseNpmRoute(path, hash) {
146
146
  // link opens, names its file by number and its lines in the fragment, as a
147
147
  // bundle's Code does: `/code/<file>#L4`. Compare names the commit compared
148
148
  // with, then its mode past the Overview, as an npm version's does:
149
- // `/compare/<commit>/code`. Without a commit,
149
+ // `/compare/<commit>/code`. An Activity view names its sub-tab past the
150
+ // first (isActivitySubtab): `/pulse/year`. Without a commit,
150
151
  // `/repo/<repo id>[?path=<directory>]`, it opens on the default
151
152
  // branch's head, or on the revision a route held in memory names (`ref`),
152
153
  // and its route then names the commit it opened. Links from before it moved
@@ -155,6 +156,15 @@ export const REPOSITORY_CODE_TABS = new Set(['code', 'search', 'treemap', 'compa
155
156
  // The tabs reading none of the directory's files, which a directory at a
156
157
  // commit shows alone.
157
158
  export const REPOSITORY_FILELESS_TABS = new Set(['commits', 'issues', 'pulls', 'agents', 'pulse', 'replay'])
159
+ // The Activity views' sub-tabs past their first: Issues' and Pull requests'
160
+ // closed ones (open, the first), Pulse's periods (a month, the first),
161
+ // Replay's commits played (the last 300, the first), and one agent's
162
+ // sessions, by its id (every agent's, `all`, the first).
163
+ const ACTIVITY_SUBTABS = new Map([['issues', new Set(['closed'])], ['pulls', new Set(['closed'])],
164
+ ['pulse', new Set(['week', 'quarter', 'year'])], ['replay', new Set(['100', '1000'])]])
165
+ const AGENT_ID = /^[a-z][a-z\d-]{0,31}$/u
166
+ export const isActivitySubtab = (tab, subtab) => tab === 'agents' ? typeof subtab === 'string' && AGENT_ID.test(subtab) && subtab !== 'all'
167
+ : ACTIVITY_SUBTABS.get(tab)?.has(subtab) === true
158
168
  const COMMIT = /^[a-f\d]{40}$/u
159
169
  const MAX_DIRECTORY = 500
160
170
  const isRepoId = value => Number.isSafeInteger(value) && value > 0
@@ -176,9 +186,10 @@ function repositoryCodePath(route) {
176
186
  if (route.commit == null) return `/repo/${route.repoId}${directory}`
177
187
  const file = tab === 'code' && isLineNumber(route.file) ? `/code/${route.file}` : tab === 'code' ? '' : `/${tab}`
178
188
  const compare = tab === 'compare' && route.compareCommit != null ? `/${route.compareCommit}${compareModeSuffix(route.compareMode)}` : ''
189
+ const subtab = isActivitySubtab(tab, route.subtab) ? `/${route.subtab}` : ''
179
190
  const lines = tab === 'code' && isLineNumber(route.file) && isLineNumber(route.line)
180
191
  ? `#L${route.line}${isLineNumber(route.endLine) && route.endLine > route.line ? `-L${route.endLine}` : ''}` : ''
181
- return `/repo/${route.repoId}/${route.commit}${file}${compare}${directory}${lines}`
192
+ return `/repo/${route.repoId}/${route.commit}${file}${compare}${subtab}${directory}${lines}`
182
193
  }
183
194
 
184
195
  function parseRepositoryCodeRoute(url) {
@@ -195,6 +206,7 @@ function parseRepositoryCodeRoute(url) {
195
206
  if (tab === 'compare') {
196
207
  return COMMIT.test(match[4]) && (match[5] == null || COMPARE_MODES.has(match[5])) ? { ...route, compareCommit: match[4], ...compareModeField(match[5]) } : null
197
208
  }
209
+ if (REPOSITORY_FILELESS_TABS.has(tab)) return match[5] == null && isActivitySubtab(tab, match[4]) ? { ...route, subtab: match[4] } : null
198
210
  const file = Number(match[4])
199
211
  return tab === 'code' && match[5] == null && /^[1-9]\d*$/u.test(match[4]) && Number.isSafeInteger(file) ? { ...route, file, ...codeLines(url.hash) } : null
200
212
  }