44
55import type { Config } from '../config.js' ;
66import type { GitHubClient } from '../github/client.js' ;
7+ import type { GitHubComment , GitHubEvent } from '../github/types.js' ;
78import type { SEPItem , StaleAnalysis } from '../types.js' ;
89import { BOT_COMMENT_MARKER } from '../types.js' ;
910import { daysBetween } from '../utils/index.js' ;
@@ -22,10 +23,18 @@ export class SEPAnalyzer {
2223 */
2324 async analyze ( item : SEPItem ) : Promise < StaleAnalysis > {
2425 const now = new Date ( ) ;
25- const daysSinceActivity = daysBetween ( item . updatedAt , now ) ;
26+
27+ // Fetch comments and events once, share across checks
28+ const comments = await this . github . getComments ( item . number ) ;
29+ const events = await this . github . getEvents ( item . number ) ;
30+
31+ // Compute days since the responsible person was last active
32+ // (not just any activity, which includes bot pings that reset updated_at)
33+ const responsibleLastActive = this . findResponsiblePersonActivity ( item , events , comments ) ;
34+ const daysSinceActivity = daysBetween ( responsibleLastActive , now ) ;
2635
2736 // Check cooldown - don't ping if we pinged recently
28- const lastPingDate = await this . getLastBotPingDate ( item . number ) ;
37+ const lastPingDate = this . findLastBotPingDate ( comments ) ;
2938 if ( lastPingDate ) {
3039 const daysSincePing = daysBetween ( lastPingDate , now ) ;
3140 if ( daysSincePing < this . config . pingCooldownDays ) {
@@ -151,20 +160,66 @@ export class SEPAnalyzer {
151160 }
152161
153162 /**
154- * Check maintainer activity on a SEP
163+ * Check a specific user's activity on a SEP
155164 */
156- async checkMaintainerActivity ( item : SEPItem , maintainerUsername : string ) : Promise < {
165+ async checkUserActivity ( item : SEPItem , username : string ) : Promise < {
157166 daysSinceActivity : number ;
158167 shouldPing : boolean ;
159168 } > {
160169 const events = await this . github . getEvents ( item . number ) ;
161170 const comments = await this . github . getComments ( item . number ) ;
162171
163- // Find last activity by this maintainer
172+ const lastActivity = this . findLastUserActivity ( username , events , comments ) ;
173+
174+ const daysSinceActivity = daysBetween ( lastActivity ?? item . createdAt , new Date ( ) ) ;
175+ const shouldPing = daysSinceActivity >= this . config . maintainerInactivityDays ;
176+
177+ return { daysSinceActivity, shouldPing } ;
178+ }
179+
180+ /**
181+ * Find the last activity date of the person responsible for the SEP.
182+ *
183+ * For 'proposal' and 'accepted' states, this is the author.
184+ * For 'draft' state, this is the first assignee (sponsor), falling back to author.
185+ *
186+ * Falls back to item.createdAt when no user-specific activity is found.
187+ */
188+ private findResponsiblePersonActivity (
189+ item : SEPItem ,
190+ events : GitHubEvent [ ] ,
191+ comments : GitHubComment [ ] ,
192+ ) : Date {
193+ const username = this . getResponsibleUsername ( item ) ;
194+ return this . findLastUserActivity ( username , events , comments ) ?? item . createdAt ;
195+ }
196+
197+ /**
198+ * Determine who the responsible person is for staleness tracking.
199+ *
200+ * For accepted SEPs, this is always the author (awaiting reference implementation).
201+ * For all other states, this is the first assignee (typically the sponsor),
202+ * falling back to the author if there are no assignees.
203+ */
204+ private getResponsibleUsername ( item : SEPItem ) : string {
205+ if ( item . state === 'accepted' ) {
206+ return item . author ;
207+ }
208+ return item . assignees [ 0 ] ?? item . author ;
209+ }
210+
211+ /**
212+ * Find the most recent activity date for a specific user, excluding bot comments.
213+ */
214+ private findLastUserActivity (
215+ username : string ,
216+ events : GitHubEvent [ ] ,
217+ comments : GitHubComment [ ] ,
218+ ) : Date | null {
164219 let lastActivity : Date | null = null ;
165220
166221 for ( const event of events ) {
167- if ( event . actor ?. login === maintainerUsername ) {
222+ if ( event . actor ?. login === username ) {
168223 const eventDate = new Date ( event . created_at ) ;
169224 if ( ! lastActivity || eventDate > lastActivity ) {
170225 lastActivity = eventDate ;
@@ -173,39 +228,30 @@ export class SEPAnalyzer {
173228 }
174229
175230 for ( const comment of comments ) {
176- if ( comment . user ?. login === maintainerUsername ) {
231+ if ( comment ?. body . includes ( BOT_COMMENT_MARKER ) ) {
232+ continue ;
233+ }
234+ if ( comment . user ?. login === username ) {
177235 const commentDate = new Date ( comment . created_at ) ;
178236 if ( ! lastActivity || commentDate > lastActivity ) {
179237 lastActivity = commentDate ;
180238 }
181239 }
182240 }
183241
184- // If no activity found, use assignment date or item update date
185- if ( ! lastActivity ) {
186- lastActivity = item . updatedAt ;
187- }
188-
189- const daysSinceActivityValue = daysBetween ( lastActivity , new Date ( ) ) ;
190- const shouldPing = daysSinceActivityValue >= this . config . maintainerInactivityDays ;
191-
192- return { daysSinceActivity : daysSinceActivityValue , shouldPing } ;
242+ return lastActivity ;
193243 }
194244
195245 /**
196- * Get the date of the last bot ping comment
246+ * Find the date of the last bot ping comment from pre-fetched comments.
197247 */
198- private async getLastBotPingDate ( issueNumber : number ) : Promise < Date | null > {
199- const comments = await this . github . getComments ( issueNumber ) ;
200-
201- // Find most recent bot comment with our marker
248+ private findLastBotPingDate ( comments : GitHubComment [ ] ) : Date | null {
202249 for ( let i = comments . length - 1 ; i >= 0 ; i -- ) {
203250 const comment = comments [ i ] ;
204251 if ( comment ?. body . includes ( BOT_COMMENT_MARKER ) ) {
205252 return new Date ( comment . created_at ) ;
206253 }
207254 }
208-
209255 return null ;
210256 }
211257}
0 commit comments