(report: SweepReport)
| 535 | } |
| 536 | |
| 537 | export function formatSweepReport(report: SweepReport): { |
| 538 | subject: string |
| 539 | message: string |
| 540 | } { |
| 541 | const high = report.suspects.filter((s) => s.tier === 'high') |
| 542 | const medium = report.suspects.filter((s) => s.tier === 'medium') |
| 543 | |
| 544 | const subject = |
| 545 | high.length > 0 |
| 546 | ? `[freebuff bot-sweep] ${high.length} high-confidence suspects (${report.totalSessions} active+queued)` |
| 547 | : `[freebuff bot-sweep] ${medium.length} medium suspects (${report.totalSessions} active+queued)` |
| 548 | |
| 549 | const lines: string[] = [] |
| 550 | lines.push(`Snapshot: ${report.generatedAt.toISOString()}`) |
| 551 | lines.push( |
| 552 | `Sessions: ${report.totalSessions} (active=${report.activeCount}, queued=${report.queuedCount})`, |
| 553 | ) |
| 554 | lines.push(`Suspects: high=${high.length}, medium=${medium.length}`) |
| 555 | lines.push('') |
| 556 | |
| 557 | // Hyphen-separated rather than column-aligned: Loops may render |
| 558 | // {{message}} as HTML and collapse whitespace, which would ruin padEnd |
| 559 | // column alignment. Separator-delimited survives both plain text and |
| 560 | // wrapped HTML. |
| 561 | const renderSuspect = (s: BotSuspect) => { |
| 562 | const gh = |
| 563 | s.githubAgeDays !== null |
| 564 | ? ` gh_age=${s.githubAgeDays.toFixed(1)}d` |
| 565 | : s.githubId === null |
| 566 | ? ' gh_age=n/a' |
| 567 | : ' gh_age=?' |
| 568 | const counter = |
| 569 | s.counterSignals.length > 0 |
| 570 | ? ` | counter: ${s.counterSignals.join(' ')}` |
| 571 | : '' |
| 572 | return ` ${s.email} — score=${s.score} age=${s.ageDays.toFixed(1)}d${gh} msgs24=${s.msgs24h} agents24=${s.distinctAgents24h} lifetime=${s.msgsLifetime} | ${s.flags.join(' ')}${counter}` |
| 573 | } |
| 574 | |
| 575 | if (high.length > 0) { |
| 576 | lines.push(`=== HIGH CONFIDENCE (${high.length}) ===`) |
| 577 | for (const s of high) lines.push(renderSuspect(s)) |
| 578 | lines.push('') |
| 579 | } |
| 580 | |
| 581 | if (medium.length > 0) { |
| 582 | lines.push(`=== MEDIUM (${medium.length}) ===`) |
| 583 | for (const s of medium) lines.push(renderSuspect(s)) |
| 584 | lines.push('') |
| 585 | } |
| 586 | |
| 587 | if (report.creationClusters.length > 0) { |
| 588 | lines.push( |
| 589 | `=== CREATION CLUSTERS (${report.creationClusters.length}) — accounts created within ${CREATION_CLUSTER_WINDOW_MS / 60000}m of each other ===`, |
| 590 | ) |
| 591 | for (const c of report.creationClusters) { |
| 592 | lines.push( |
| 593 | ` ${c.windowStart.toISOString()} .. ${c.windowEnd.toISOString()} n=${c.emails.length}`, |
| 594 | ) |
no test coverage detected