@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preventive/triage",
3
- "version": "1.0.0-alpha.49",
3
+ "version": "1.0.0-alpha.50",
4
4
  "description": "Client & relay server for triaging of automated reports",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "author": {
@@ -195,6 +195,7 @@
195
195
  "server-managed/repository-squashes.ts",
196
196
  "server-managed/repository-issues.ts",
197
197
  "server-managed/repository-pulse.ts",
198
+ "server-managed/repository-pulls.ts",
198
199
  "server-managed/repository-aliases.ts",
199
200
  "server-managed/repository-discovery.ts",
200
201
  "server-managed/repository-policy.ts",
@@ -280,6 +281,8 @@
280
281
  "common/linked-workspace-groups.js",
281
282
  "common/managed/agent-tasks.ts",
282
283
  "common/managed/comments.ts",
284
+ "common/managed/finding-detail.d.ts",
285
+ "common/managed/finding-detail.js",
283
286
  "common/managed/github-agents.ts",
284
287
  "common/managed/github-logins.ts",
285
288
  "common/managed/npm-packages.d.ts",
@@ -1,154 +1,49 @@
1
1
  // The commits agents' pull requests brought to a repository's default
2
- // branch, for its Pulse (repository-pulse.ts commitStats): an agent that
3
- // commits as whoever ran it (Codex's own) leaves its commits no mark, its
4
- // pull request's branch alone naming it (app-agents.ts
5
- // AGENT_BRANCH_PREFIXES), or a label one who ran it gave it naming the agent
6
- // (agent-tasks.ts labelAgents), as the Agents tab tells an agent's work; or
7
- // its author, an account agents work as (github-agents.ts AGENT_OPERATED),
8
- // or any GitHub App's bot.
9
- import { labelAgents } from '../common/managed/agent-tasks.ts'
10
- import { GITHUB_AGENTS, agentOperated } from '../common/managed/github-agents.ts'
11
- import { AGENT_BRANCH_PREFIXES } from './app-agents.ts'
12
- import { type HistoryCommit, githubGraphql } from './repository-history.ts'
2
+ // branch, for its Pulse (repository-pulse.ts commitStats), as the pull
3
+ // requests kept tell them (repository-pulls.ts readPulls: whose each is,
4
+ // and what one merged brought).
5
+ import type { PullList } from './repository-pulls.ts'
6
+ import type { HistoryCommit } from './repository-history.ts'
13
7
 
14
- // The merged pull requests read an answer, a page a query, as many queries
15
- // as MERGED_QUERIES, the rest left for the next to ask; of each, as many of
16
- // its latest commits as MERGED_COMMITS.
17
- const MERGED_PAGE = 100
18
- const MERGED_QUERIES = 5
19
- const MERGED_COMMITS = 100
20
- // Of each, as many labels as MERGED_LABELS, GitHub's most a page, a label
21
- // naming any agent the app knows making it an agent's.
22
- const MERGED_LABELS = 100
23
8
  // How long before or after a pull request rebased onto the branch was merged
24
9
  // its copies are committed.
25
10
  const REBASE_SKEW_MS = 10 * 60_000
26
- const LABEL_AGENTS = Object.keys(GITHUB_AGENTS)
27
- const SHA = /^[a-f\d]{40}$/u
28
-
29
- const count = (value: unknown) => Number.isSafeInteger(value) && (value as number) >= 0 ? value as number : 0
30
- const timeOf = (value: unknown) => {
31
- const at = typeof value === 'string' ? Date.parse(value) : Number.NaN
32
- return Number.isSafeInteger(at) ? at : null
33
- }
34
11
 
35
12
  // The agents' pull requests merged since a year's start, as read so far
36
13
  // (agentMerges): the commits each brought, and when it was merged; those
37
14
  // rebased onto the branch by their last commit there, when merged, and
38
- // their commits' subjects, the oldest first (agentCommits); when the newest
39
- // read was last updated, those updated since read again (`through`, null
40
- // till all are read once), and where those read reach back no further than
41
- // a period needed, when the oldest of them was updated (`reach`), every
42
- // pull request merged since read (one is updated when merged, or after);
43
- // and a read not finished: where it goes on (`after`), when the newest it
44
- // began with was updated (`from`), the oldest it reads to (`stop`), how many
45
- // it has read (`read`) and when the oldest of them was updated (`to`). And
46
- // of every pull
47
- // request merged, an agent's or not, its merge commit and when (`pulls`),
48
- // and one rebased so (`pullRebases`), for the commits pushed to the branch
49
- // directly to be told (directCommits).
15
+ // their commits' subjects, the oldest first (agentCommits); and how far the
16
+ // pull requests are read, as the list tells (repository-pulls.ts PullList:
17
+ // `through`, `reach`, `next`). And of every pull request merged, an agent's
18
+ // or not, its merge commit, when and its number (`pulls`, by which the
19
+ // Commits tab tells those squashed too, http.ts pulseMerges; none of one
20
+ // kept before numbers were), and one rebased so (`pullRebases`), for the
21
+ // commits pushed to the branch directly to be told (directCommits).
50
22
  export interface AgentMerges { merges: [string, number][]; rebases: Rebase[]; through: number | null; reach?: number
51
- next: { after: string; from: number; stop: number; read: number; to?: number } | null; pulls?: [string, number][]; pullRebases?: Rebase[] }
23
+ next: { after: string; from: number; stop: number; read: number; to?: number } | null; pulls?: [sha: string, at: number, number?: number][]; pullRebases?: Rebase[] }
52
24
  // A pull request rebased onto the branch: its last commit there, when it was
53
25
  // merged, its latest commits' subjects, the oldest first, and how many more
54
26
  // it had than those read (`more`).
55
27
  type Rebase = [sha: string, at: number, subjects: string[], more?: number]
56
28
 
57
- const MERGED_QUERY = `query($owner: String!, $name: String!, $after: String) {
58
- repository(owner: $owner, name: $name) {
59
- pullRequests(states: MERGED, first: ${MERGED_PAGE}, after: $after, orderBy: { field: UPDATED_AT, direction: DESC }) {
60
- pageInfo { hasNextPage endCursor }
61
- nodes { updatedAt mergedAt headRefName author { __typename login ... on User { databaseId } } labels(first: ${MERGED_LABELS}) { nodes { name } }
62
- mergeCommit { oid parents { totalCount } }
63
- commits(last: ${MERGED_COMMITS}) { totalCount nodes { commit { oid messageHeadline changedFilesIfAvailable } } } }
64
- }
65
- }
66
- }`
67
- type MergedPage = { repository?: { pullRequests?: { pageInfo?: { hasNextPage?: unknown; endCursor?: unknown }
68
- nodes?: ({ updatedAt?: unknown; mergedAt?: unknown; headRefName?: unknown; author?: { __typename?: unknown; login?: unknown; databaseId?: unknown } | null
69
- labels?: { nodes?: ({ name?: unknown } | null)[] } | null
70
- mergeCommit?: { oid?: unknown; parents?: { totalCount?: unknown } | null } | null
71
- commits?: { totalCount?: unknown; nodes?: ({ commit?: { oid?: unknown; messageHeadline?: unknown; changedFilesIfAvailable?: unknown } | null } | null)[] } | null
72
- } | null)[] } | null } | null }
73
- const agentBranch = (name: unknown) => typeof name === 'string' && AGENT_BRANCH_PREFIXES.some(prefix => name.startsWith(prefix))
74
- type MergedNode = NonNullable<NonNullable<NonNullable<NonNullable<MergedPage['repository']>['pullRequests']>['nodes']>[number]>
75
- // Whether a pull request is an agent's: by its branch, a label naming one,
76
- // or its author, an account agents work as or a bot (GitHub's GraphQL API
77
- // naming a bot by its app's login alone, its type telling it).
78
- const agentPull = ({ headRefName, labels, author }: MergedNode) => agentBranch(headRefName)
79
- || labelAgents((labels?.nodes ?? []).map(label => label?.name), LABEL_AGENTS).length > 0 || author?.__typename === 'Bot'
80
- || agentOperated(typeof author?.login === 'string' ? author.login : null, Number.isSafeInteger(author?.databaseId) ? author!.databaseId as number : null)
81
-
82
- // The commits the pull requests merged since `since` from an agent's branch,
83
- // labelled for one, or opened by an account agents work as, brought: each its merge commit, a squash merge's one
84
- // commit; where it was
85
- // merged with a merge commit, which a year counts none of, its own commits,
86
- // its latest MERGED_COMMITS; and of one of more commits whose merge commit
87
- // has one parent, its commits' subjects, for those it was rebased as to be
88
- // told by (agentCommits), those of its empty commits left out, as a rebase
89
- // leaves them. Those merged are read the most recently updated
90
- // first (a pull request is updated when merged, or after), on from what was
91
- // `kept`: till one updated before the year, or before the newest read before
92
- // was, as many queries an answer as MERGED_QUERIES, a read not finished
93
- // going on from where it was left by the next, or once those merged since
94
- // `need` are all read; one read again whose label no longer names an agent,
95
- // what it was read to bring forgotten.
96
- export async function agentMerges(fullName: string, token: string, since: number, kept: AgentMerges | null,
97
- fetchImpl: typeof fetch = globalThis.fetch, need = since): Promise<AgentMerges> {
98
- const [owner, name] = fullName.split('/') as [string, string]
99
- const merges = new Map((kept?.merges ?? []).filter(([, at]) => at >= since))
100
- const rebases = new Map((kept?.rebases ?? []).filter(([, at]) => at >= since).map(([sha, at, subjects, more = 0]) => [sha, [at, subjects, more] as const]))
101
- const pulls = new Map((kept?.pulls ?? []).filter(([, at]) => at >= since))
102
- const pullRebases = new Map((kept?.pullRebases ?? []).filter(([, at]) => at >= since).map(([sha, at, subjects, more = 0]) => [sha, [at, subjects, more] as const]))
103
- // Those read reaching back no further than they needed, and `need`
104
- // further: read again from the newest on, as far back as it needs.
105
- const further = !kept?.next && kept?.reach !== undefined && kept.reach >= need
106
- const stop = kept?.next?.stop ?? (further ? since : Math.max(since, kept?.through ?? since))
107
- const listed = (map: typeof rebases) => [...map].map(([sha, [at, subjects, more]]): Rebase => more > 0 ? [sha, at, subjects, more] : [sha, at, subjects])
108
- const result = (through: number | null, next: AgentMerges['next'], reach?: number): AgentMerges => ({ merges: [...merges], rebases: listed(rebases), through,
109
- ...reach === undefined ? {} : { reach }, next, pulls: [...pulls], pullRebases: listed(pullRebases) })
110
- let { after = null, from = null, read = 0, to = null }: { after?: string | null; from?: number | null; read?: number; to?: number | null } = kept?.next ?? {}
111
- for (let queries = 0; queries < MERGED_QUERIES; queries++) {
112
- const page = (await githubGraphql(MERGED_QUERY, { owner, name, after }, token, fetchImpl) as MergedPage | null)?.repository?.pullRequests
113
- let done = page?.pageInfo?.hasNextPage !== true || typeof page.pageInfo.endCursor !== 'string'
114
- for (const node of page?.nodes ?? []) {
115
- const updated = timeOf(node?.updatedAt)
116
- if (updated === null) continue
117
- if (updated < stop) { done = true; break }
118
- from ??= updated
119
- to = updated
120
- read++
121
- const [merged, merge] = [timeOf(node?.mergedAt), node?.mergeCommit?.oid]
122
- if (!node || merged === null || merged < since || typeof merge !== 'string' || !SHA.test(merge)) continue
123
- const own = (node.commits?.nodes ?? []).map(item => item?.commit)
124
- const merging = count(node.mergeCommit?.parents?.totalCount) > 1
125
- const brought = [merge, ...merging ? own.map(commit => commit?.oid) : []].filter((oid): oid is string => typeof oid === 'string' && SHA.test(oid))
126
- const copied = own.filter(commit => commit?.changedFilesIfAvailable !== 0)
127
- // Of one of more commits than its latest read, how many more it had.
128
- const more = Math.max(0, count(node.commits?.totalCount) - own.length)
129
- const subjects = !merging && (copied.length > 1 || more > 0) && copied.length > 0 && copied.every(commit => typeof commit?.messageHeadline === 'string')
130
- ? copied.map(commit => commit!.messageHeadline as string) : null
131
- pulls.set(merge, merged)
132
- if (subjects) pullRebases.set(merge, [merged, subjects, more])
133
- // One an agent's no more, its label taken off (which updates it), what
134
- // it was read to bring none of an agent's.
135
- if (!agentPull(node)) {
136
- for (const oid of brought.filter(sha => merges.get(sha) === merged)) merges.delete(oid)
137
- if (rebases.get(merge)?.[0] === merged) rebases.delete(merge)
138
- continue
139
- }
140
- for (const oid of brought) merges.set(oid, merged)
141
- if (subjects) rebases.set(merge, [merged, subjects, more])
142
- }
143
- // Read as far back as it reads to: the year's start, every one since
144
- // read; else as far as those read before reached.
145
- if (done) return result(from ?? stop, null, stop > since ? kept?.reach : undefined)
146
- after = page!.pageInfo!.endCursor as string
147
- // Read as far back as `need`: finished, so far back, those updated
148
- // since read by the next as ever.
149
- if (need > since && to !== null && to < need) return result(Math.max(from ?? stop, kept?.through ?? Number.NEGATIVE_INFINITY), null, to)
29
+ // What the pull requests merged since `since` of those read (`list`) brought:
30
+ // an agent's, its merge commit, a squash merge's one commit, and one merged
31
+ // with a merge commit, its own commits too; and every one's merge commit,
32
+ // those rebased by their commits' subjects.
33
+ export function agentMerges(list: PullList, since: number): AgentMerges {
34
+ const merges = new Map<string, number>()
35
+ const [rebases, pulls, pullRebases]: [Rebase[], [string, number, number][], Rebase[]] = [[], [], []]
36
+ for (const { number, merged } of list.pulls) {
37
+ if (!merged || merged.at < since) continue
38
+ const { sha, at, subjects, more = 0 } = merged
39
+ const rebase: Rebase | null = subjects ? more > 0 ? [sha, at, subjects, more] : [sha, at, subjects] : null
40
+ pulls.push([sha, at, number])
41
+ if (rebase) pullRebases.push(rebase)
42
+ if (!merged.agent) continue
43
+ for (const oid of [sha, ...merged.own ?? []]) merges.set(oid, at)
44
+ if (rebase) rebases.push(rebase)
150
45
  }
151
- return result(kept?.through ?? null, { after: after!, from: from ?? stop, stop, read, ...to === null ? {} : { to } }, kept?.reach)
46
+ return { merges: [...merges], rebases, through: list.through, ...list.reach === undefined ? {} : { reach: list.reach }, next: list.next, pulls, pullRebases }
152
47
  }
153
48
 
154
49
  // Whether two reads of the pull requests merged (agentMerges) bring the
@@ -204,7 +99,7 @@ export function agentCommits(read: AgentMerges, commits: HistoryCommit[]): Set<s
204
99
  // commit, or the copies of one rebased onto it, as agentCommits tells
205
100
  // them); those a merge brought from its other parents, not.
206
101
  export function directCommits(read: AgentMerges, commits: HistoryCommit[], head: string): Set<string> {
207
- const pulled = agentCommits({ ...read, merges: read.pulls ?? [], rebases: read.pullRebases ?? [] }, commits)
102
+ const pulled = agentCommits({ ...read, merges: (read.pulls ?? []).map(([sha, at]) => [sha, at]), rebases: read.pullRebases ?? [] }, commits)
208
103
  const bySha = new Map(commits.map(commit => [commit.sha, commit]))
209
104
  const direct = new Set<string>()
210
105
  for (let commit = bySha.get(head); commit; commit = bySha.get(commit.parents[0] ?? '')) {
@@ -15,3 +15,7 @@ export const APP_AGENTS: readonly (AppAgent & AppAgentMarks)[] = Object.entries<
15
15
  // The prefixes of the branches agents work on: Claude Code's and Codex's,
16
16
  // and the app agents'.
17
17
  export const AGENT_BRANCH_PREFIXES = ['claude/', 'codex/', ...APP_AGENTS.map(agent => agent.prefix)]
18
+ // What the marks agents leave on commits are read by, as the history keeps
19
+ // them (repository-history.ts commitMarks): bumped as how they are told
20
+ // changes, so that the Agents tab reads again the pages kept by older rules.
21
+ export const AGENT_MARKS_READ = 1
@@ -7,10 +7,11 @@ import { APP_AGENTS } from './app-agents.ts'
7
7
  import { type NamedAccount, commitAccount } from './github-accounts.ts'
8
8
 
9
9
  export const MAX_COAUTHORS = 3
10
- // The rules co-authors are read by, the agents' addresses and names among
11
- // them: what was kept read by others (history pages, squashed pull requests)
12
- // read again by these, once raised.
13
- export const COAUTHORS_READ = 4
10
+ // The rules who a commit names beside its author is read by (coauthorFacts),
11
+ // and how they are kept: what was kept read by others (history pages,
12
+ // squashed pull requests) read again by these, once raised. The agents'
13
+ // accounts, addresses and names are none of them (coauthorsOf).
14
+ export const COAUTHORS_READ = 5
14
15
  const MAX_NAME = 256
15
16
  const textOf = (value: unknown, max: number) => typeof value === 'string' && value.trim() ? value.trim().slice(0, max) : null
16
17
 
@@ -41,30 +42,51 @@ export const coauthorTrailers = (message: unknown) => typeof message === 'string
41
42
  const makerAgent = ({ name, email, account }: Maker): GithubAgentId | null => githubAgent(account.login, account.id) ?? emailAgent(email)
42
43
  ?? (account.login ? null : githubAgent(null, null, typeof name === 'string' ? name : null))
43
44
 
44
- // Who made a commit, past its author (`author`), as its message names them:
45
- // its co-authors, those GitHub's GraphQL API lists past the author (`listed`,
46
- // the addresses its `Co-authored-by:` trailers give, by the accounts GitHub
47
- // knows them by), else its trailers (agent-tasks.ts trailerBlock), an account's no-reply
48
- // address naming it;
49
- // then the agents it marks, Claude's and Codex's as the Agents tab reads them
45
+ // Who a commit names beside its author, as its history keeps them
46
+ // (repository-history.ts, `coauthorFacts`), its message never kept: its
47
+ // co-authors (`named`), those GitHub's GraphQL API lists past the author
48
+ // (`listed`, the addresses its `Co-authored-by:` trailers give, by the
49
+ // accounts GitHub knows them by), else its trailers (agent-tasks.ts
50
+ // trailerBlock), an account's no-reply address naming it, as many as
51
+ // MAX_NAMED, each by name, address and account; and the agents its message
52
+ // marks (`markedBy`), Claude's and Codex's as the Agents tab reads them
50
53
  // (agent-tasks.ts agentSignals: their sessions' and tasks' links, Claude's
51
54
  // co-author and "Generated with" lines), the app agents' by their
52
- // fingerprints, Cursor's by its agents' links. Each the agent it is where
53
- // known, and once, the author none of them, as many as MAX_COAUTHORS: two
54
- // are one by the first of their agent, account, address and name both name,
55
- // two people of one name with accounts or addresses of their own two.
56
- export function coauthorsOf(message: unknown, author: Maker, listed: Maker[] | null = null): Coauthor[] {
55
+ // fingerprints, Cursor's by its agents' links. Which of them are agents,
56
+ // and so the same as another, is told as they are answered (coauthorsOf), so
57
+ // that the agents' accounts, addresses and names change none kept.
58
+ export interface Named { name: string | null; email: string | null; login: string | null; id: number | null }
59
+ export const MAX_NAMED = 10
60
+ export function coauthorFacts(message: unknown, listed: Maker[] | null = null): { named: Named[]; markedBy: string[] } {
57
61
  const text = typeof message === 'string' ? message.replaceAll('\r\n', '\n') : ''
58
- const named = listed ?? coauthorTrailers(text).map(({ name, email }) => ({ name, email, account: commitAccount({ email }) }))
62
+ const makers = listed ?? coauthorTrailers(text).map(({ name, email }) => ({ name, email, account: commitAccount({ email }) }))
59
63
  const signals = agentSignals([text], FINGERPRINTED)
60
- const marked = [...signals.claude || signals.sessions.length > 0 ? ['claude'] : [], ...signals.tasks.length > 0 || signals.codex ? ['codex'] : [],
61
- ...CURSOR_AGENT.test(text) ? ['cursor'] : [], ...signals.agents]
64
+ return {
65
+ named: makers.slice(0, MAX_NAMED).map(({ name, email, account }) => ({ name: textOf(name, MAX_NAME), email: textOf(email, MAX_NAME), login: account.login, id: account.id })),
66
+ markedBy: [...signals.claude || signals.sessions.length > 0 ? ['claude'] : [], ...signals.tasks.length > 0 || signals.codex ? ['codex'] : [],
67
+ ...CURSOR_AGENT.test(text) ? ['cursor'] : [], ...signals.agents],
68
+ }
69
+ }
70
+
71
+ // A commit as its history keeps it: its author by name, address and account,
72
+ // and who it names beside them (coauthorFacts), absent from pages kept before.
73
+ export interface CommitFacts { authorName: string | null; authorEmail?: string | null; authorLogin: string | null; authorId?: number | null; named?: Named[]
74
+ markedBy?: string[] }
75
+
76
+ // Who made a commit, past its author, as its history keeps them
77
+ // (`commit`, coauthorFacts): its co-authors, then the agents its message
78
+ // marks, each the agent it is where known, by the agents the app knows now,
79
+ // and once, the author none of them, as many as MAX_COAUTHORS: two are one by
80
+ // the first of their agent, account, address and name both name, two people
81
+ // of one name with accounts or addresses of their own two.
82
+ export function coauthorsOf({ authorName, authorEmail = null, authorLogin, authorId = null, named = [], markedBy = [] }: CommitFacts): Coauthor[] {
62
83
  type Identity = { agent: string | null; login: string | null; email: string | null; name: string | null }
63
84
  const identity = (agent: string | null, login: string | null, name: string | null, email: unknown = null): Identity => ({ agent, login: login?.toLowerCase() ?? null,
64
85
  email: typeof email === 'string' && email.trim() ? email.trim().toLowerCase() : null, name: name?.toLowerCase() ?? null })
65
86
  const same = (a: Identity, b: Identity) => a.agent && b.agent ? a.agent === b.agent : a.login && b.login ? a.login === b.login
66
87
  : a.email && b.email ? a.email === b.email : a.name !== null && a.name === b.name
67
- const seen = [identity(makerAgent(author), author.account.login, textOf(author.name, MAX_NAME), author.email)]
88
+ const author: Maker = { name: authorName, email: authorEmail, account: { login: authorLogin, id: authorId } }
89
+ const seen = [identity(makerAgent(author), authorLogin, textOf(authorName, MAX_NAME), authorEmail)]
68
90
  const coauthors: Coauthor[] = []
69
91
  const add = (coauthor: Coauthor, email: unknown = null) => {
70
92
  const who = identity(coauthor.agent ?? null, coauthor.login, coauthor.name, email)
@@ -72,13 +94,13 @@ export function coauthorsOf(message: unknown, author: Maker, listed: Maker[] | n
72
94
  seen.push(who)
73
95
  coauthors.push(coauthor)
74
96
  }
75
- for (const maker of named) {
76
- const agent = makerAgent(maker)
97
+ for (const { name, email, ...account } of named) {
98
+ const agent = makerAgent({ name, email, account })
77
99
  // An agent known by its address where GitHub names someone else's
78
100
  // account for it (Codex's), as itself, not that account.
79
- const { login, id } = agent && maker.account.login && githubAgent(maker.account.login, maker.account.id) !== agent ? { login: null, id: null } : maker.account
80
- add({ name: textOf(maker.name, MAX_NAME), login, id, ...agent ? { agent } : {} }, maker.email)
101
+ const { login, id } = agent && account.login && githubAgent(account.login, account.id) !== agent ? { login: null, id: null } : account
102
+ add({ name, login, id, ...agent ? { agent } : {} }, email)
81
103
  }
82
- for (const agent of marked) add({ name: GITHUB_AGENTS[agent as GithubAgentId].name, login: null, id: null, agent })
104
+ for (const agent of markedBy) if (Object.hasOwn(GITHUB_AGENTS, agent)) add({ name: GITHUB_AGENTS[agent as GithubAgentId].name, login: null, id: null, agent })
83
105
  return coauthors
84
106
  }