@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.
- package/common/managed/finding-detail.d.ts +2 -0
- package/common/managed/finding-detail.js +32 -0
- package/common/managed/github-agents.ts +6 -3
- package/common/managed/routes.js +14 -2
- package/out/activity.js +4 -4
- package/out/{chunk-ULIF7A7N.js → chunk-2WDZPXVL.js} +1 -1
- package/out/chunk-4XMP7YVH.js +36 -0
- package/out/chunk-6S2U2UYG.js +232 -0
- package/out/{chunk-IO7JTZ6O.js → chunk-75YLHBCM.js} +1 -1
- package/out/chunk-FC65TPUB.js +1 -0
- package/out/{chunk-O6M574JI.js → chunk-FKBXNUW4.js} +1 -1
- package/out/{chunk-PBSOL2SL.js → chunk-HTBYHSPG.js} +1 -1
- package/out/{chunk-SZD5CW5R.js → chunk-UK7RGJT6.js} +2 -2
- package/out/client-managed-import.js +1 -1
- package/out/client-managed.js +2 -2
- package/out/compare.js +14 -14
- package/out/dialogs.js +34 -30
- package/out/graph.js +1 -1
- package/out/index.html +6 -6
- package/out/scan.js +2 -2
- package/out/view.css +1 -1
- package/out/view.js +15 -15
- package/package.json +4 -1
- package/server-managed/agent-merges.ts +31 -136
- package/server-managed/app-agents.ts +4 -0
- package/server-managed/commit-coauthors.ts +46 -24
- package/server-managed/http.ts +115 -37
- package/server-managed/npm-kept.ts +8 -0
- package/server-managed/public-workspace.ts +2 -2
- package/server-managed/repository-agents.ts +189 -57
- package/server-managed/repository-history.ts +51 -13
- package/server-managed/repository-pulls.ts +156 -0
- package/server-managed/repository-pulse.ts +3 -2
- package/server-managed/repository-squashes.ts +29 -6
- package/server-managed/team-reports.ts +33 -18
- package/out/chunk-BC2UCLFJ.js +0 -1
- package/out/chunk-C2AVODGZ.js +0 -36
- 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.
|
|
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)
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
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);
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
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
|
|
11
|
-
//
|
|
12
|
-
// read again by these, once raised.
|
|
13
|
-
|
|
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
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
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.
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
export
|
|
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
|
|
62
|
+
const makers = listed ?? coauthorTrailers(text).map(({ name, email }) => ({ name, email, account: commitAccount({ email }) }))
|
|
59
63
|
const signals = agentSignals([text], FINGERPRINTED)
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
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
|
|
76
|
-
const agent = makerAgent(
|
|
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 &&
|
|
80
|
-
add({ name
|
|
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
|
|
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
|
}
|