limn-review 0.1.123
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/LICENSE +110 -0
- package/README.md +133 -0
- package/dist/ai/runtime.js +535 -0
- package/dist/api/plugins/auth.js +169 -0
- package/dist/api/plugins/error-handler.js +114 -0
- package/dist/api/plugins/rate-limit.js +855 -0
- package/dist/api/plugins/security.js +321 -0
- package/dist/api/routes/activity.js +103 -0
- package/dist/api/routes/ai-runtime.js +67 -0
- package/dist/api/routes/auth.js +194 -0
- package/dist/api/routes/billing.js +209 -0
- package/dist/api/routes/bot-triage.js +1289 -0
- package/dist/api/routes/conflicts.js +730 -0
- package/dist/api/routes/contact.js +318 -0
- package/dist/api/routes/daily-brief.js +58 -0
- package/dist/api/routes/flow.js +71 -0
- package/dist/api/routes/health.js +7 -0
- package/dist/api/routes/insights.js +200 -0
- package/dist/api/routes/me.js +458 -0
- package/dist/api/routes/mentions.js +22 -0
- package/dist/api/routes/mergers.js +8 -0
- package/dist/api/routes/ml-labels.js +109 -0
- package/dist/api/routes/open-prs.js +35 -0
- package/dist/api/routes/prs.js +1633 -0
- package/dist/api/routes/reactions.js +217 -0
- package/dist/api/routes/repos.js +563 -0
- package/dist/api/routes/search.js +41 -0
- package/dist/api/routes/threads.js +118 -0
- package/dist/api/routes/timeline.js +150 -0
- package/dist/api/routes/users.js +67 -0
- package/dist/api/routes/webhooks.js +181 -0
- package/dist/api/routes/workspaces.js +322 -0
- package/dist/app.js +274 -0
- package/dist/ascii.js +19 -0
- package/dist/auth/account.js +482 -0
- package/dist/auth/crypto.js +46 -0
- package/dist/bot-triage/resolve.js +59 -0
- package/dist/cli.js +388 -0
- package/dist/coding/activity.js +80 -0
- package/dist/coding/agent.js +288 -0
- package/dist/coding/ai-fix/auto-fix.js +212 -0
- package/dist/coding/ai-fix/manager.js +370 -0
- package/dist/coding/ai-fix/persist.js +213 -0
- package/dist/coding/ai-fix/pr-context.js +58 -0
- package/dist/coding/ai-fix/prompts.js +138 -0
- package/dist/coding/ai-fix/review-seed.js +357 -0
- package/dist/coding/ai-fix/routes.js +425 -0
- package/dist/coding/git-ops.js +321 -0
- package/dist/coding/git.js +184 -0
- package/dist/coding/merge.js +262 -0
- package/dist/coding/safe-path.js +75 -0
- package/dist/coding/schema.js +43 -0
- package/dist/config.js +648 -0
- package/dist/conflict/diff.js +304 -0
- package/dist/conflict/git.js +118 -0
- package/dist/conflict/hash.js +73 -0
- package/dist/conflict/janitor.js +218 -0
- package/dist/conflict/land.js +675 -0
- package/dist/conflict/model-types.js +2 -0
- package/dist/conflict/model.js +1081 -0
- package/dist/conflict/parse.js +142 -0
- package/dist/conflict/seam.js +136 -0
- package/dist/conflict/session.js +532 -0
- package/dist/conflict/suggestion.js +398 -0
- package/dist/conflict/tokens.js +72 -0
- package/dist/data-dir.js +112 -0
- package/dist/db/actor-lanes.js +213 -0
- package/dist/db/automation-ids.js +97 -0
- package/dist/db/automation-output.js +349 -0
- package/dist/db/blast-radius-query.js +36 -0
- package/dist/db/blast-radius.js +226 -0
- package/dist/db/bot-dormancy.js +115 -0
- package/dist/db/bot-overlap.js +579 -0
- package/dist/db/bot-volume.js +722 -0
- package/dist/db/bot-window.js +35 -0
- package/dist/db/branch-queries.js +310 -0
- package/dist/db/change-shape.js +243 -0
- package/dist/db/changepoint.js +75 -0
- package/dist/db/cleanup.js +36 -0
- package/dist/db/client.js +102 -0
- package/dist/db/code-loc.js +388 -0
- package/dist/db/credits.js +79 -0
- package/dist/db/daily-brief.js +406 -0
- package/dist/db/dependency-cards.js +145 -0
- package/dist/db/erase-account.js +255 -0
- package/dist/db/export-account.js +189 -0
- package/dist/db/failing-checks.js +124 -0
- package/dist/db/file-coupling.js +197 -0
- package/dist/db/flow-detail.js +661 -0
- package/dist/db/flow-pointers.js +197 -0
- package/dist/db/flow-settings.js +67 -0
- package/dist/db/forecast.js +114 -0
- package/dist/db/line-overlap.js +81 -0
- package/dist/db/merge-queue.js +189 -0
- package/dist/db/migrate.js +17 -0
- package/dist/db/migrations/0000_purple_ben_grimm.sql +155 -0
- package/dist/db/migrations/0001_colorful_ozymandias.sql +31 -0
- package/dist/db/migrations/0002_famous_sersi.sql +9 -0
- package/dist/db/migrations/0003_clever_shinobi_shaw.sql +3 -0
- package/dist/db/migrations/0004_pale_scalphunter.sql +1 -0
- package/dist/db/migrations/0005_daffy_guardian.sql +2 -0
- package/dist/db/migrations/0006_cute_violations.sql +45 -0
- package/dist/db/migrations/0007_dark_anthem.sql +2 -0
- package/dist/db/migrations/0008_multitenant_accounts.sql +44 -0
- package/dist/db/migrations/0009_lean_bodies.sql +13 -0
- package/dist/db/migrations/0010_lean_bodies_nullable.sql +56 -0
- package/dist/db/migrations/0011_pr_diff_size.sql +10 -0
- package/dist/db/migrations/0012_findings_included_default.sql +7 -0
- package/dist/db/migrations/0013_review_routing.sql +9 -0
- package/dist/db/migrations/0014_account_last_active.sql +6 -0
- package/dist/db/migrations/0015_repos_inbox_watch.sql +9 -0
- package/dist/db/migrations/0016_repo_viewer_permission.sql +7 -0
- package/dist/db/migrations/0017_account_display_name.sql +7 -0
- package/dist/db/migrations/0018_finding_comment_kind.sql +7 -0
- package/dist/db/migrations/0019_finding_file_in_diff.sql +7 -0
- package/dist/db/migrations/0020_claude_review_cost_telemetry.sql +11 -0
- package/dist/db/migrations/0021_account_feed_last_seen.sql +7 -0
- package/dist/db/migrations/0022_ci_status_events.sql +21 -0
- package/dist/db/migrations/0023_ai_usage.sql +21 -0
- package/dist/db/migrations/0024_pr_head_ref_name.sql +6 -0
- package/dist/db/migrations/0025_account_billing.sql +7 -0
- package/dist/db/migrations/0026_ai_credit_allowance.sql +5 -0
- package/dist/db/migrations/0027_user_github_type.sql +6 -0
- package/dist/db/migrations/0028_bot_review_classification.sql +21 -0
- package/dist/db/migrations/0029_bot_mute_rules.sql +19 -0
- package/dist/db/migrations/0030_teams.sql +32 -0
- package/dist/db/migrations/0031_events_pr_idx.sql +7 -0
- package/dist/db/migrations/0032_benchmark_contributions.sql +29 -0
- package/dist/db/migrations/0033_thread_addressed_confidence.sql +9 -0
- package/dist/db/migrations/0034_thread_resolved_at.sql +6 -0
- package/dist/db/migrations/0035_pr_first_review_requested_at.sql +5 -0
- package/dist/db/migrations/0036_search_index.sql +25 -0
- package/dist/db/migrations/0037_author_indexes.sql +15 -0
- package/dist/db/migrations/0038_auto_merge_requests.sql +37 -0
- package/dist/db/migrations/0039_branch_status.sql +44 -0
- package/dist/db/migrations/0040_pr_review_decision.sql +10 -0
- package/dist/db/migrations/0041_branch_commit_detail.sql +38 -0
- package/dist/db/migrations/0042_repo_reviewers.sql +176 -0
- package/dist/db/migrations/0043_account_reviewers.sql +131 -0
- package/dist/db/migrations/0044_workspaces.sql +138 -0
- package/dist/db/migrations/0045_workspace_reviewers.sql +180 -0
- package/dist/db/migrations/0046_drop_inbox_watch.sql +25 -0
- package/dist/db/migrations/0047_ml_comment_labels.sql +40 -0
- package/dist/db/migrations/0048_ml_vendor_severity.sql +31 -0
- package/dist/db/migrations/0049_seat_pricing_repo_description.sql +21 -0
- package/dist/db/migrations/0050_users_app_slug.sql +12 -0
- package/dist/db/migrations/0051_auto_merge_queue.sql +17 -0
- package/dist/db/migrations/0052_trunk_ci_status_events.sql +43 -0
- package/dist/db/migrations/0053_automation_roles.sql +79 -0
- package/dist/db/migrations/0054_automation_vendor_kinds.sql +514 -0
- package/dist/db/migrations/0055_auto_merge_phase.sql +12 -0
- package/dist/db/migrations/0056_pr_mentions.sql +22 -0
- package/dist/db/migrations/0057_large_pr_threshold.sql +14 -0
- package/dist/db/migrations/0058_pending_mute.sql +53 -0
- package/dist/db/migrations/0059_merge_queue_state.sql +33 -0
- package/dist/db/migrations/0060_drop_my_turn_dismissals.sql +33 -0
- package/dist/db/migrations/0061_auto_merge_durable_anchors.sql +27 -0
- package/dist/db/migrations/0062_blast_radius_config.sql +21 -0
- package/dist/db/migrations/0063_repo_file_coupling.sql +33 -0
- package/dist/db/migrations/0064_pr_content_kind.sql +21 -0
- package/dist/db/migrations/0065_workspace_flow_settings.sql +13 -0
- package/dist/db/migrations/0066_review_request_events.sql +31 -0
- package/dist/db/migrations/0067_pr_security_signals.sql +86 -0
- package/dist/db/migrations/0068_my_turn_settings.sql +18 -0
- package/dist/db/migrations/0069_my_turn_dismissals.sql +41 -0
- package/dist/db/migrations/0070_claude_review_follow_up_ticket.sql +22 -0
- package/dist/db/migrations/0071_claude_review_trigger.sql +11 -0
- package/dist/db/migrations/0072_commit_message_headline.sql +13 -0
- package/dist/db/migrations/0073_claude_review_chat.sql +31 -0
- package/dist/db/migrations/0074_adopt_agentic_tables.sql +98 -0
- package/dist/db/migrations/0075_review_lens_drop_learnings.sql +15 -0
- package/dist/db/migrations/0076_claude_review_threads.sql +17 -0
- package/dist/db/migrations/0077_claude_review_ci_failures.sql +10 -0
- package/dist/db/migrations/0078_ai_fix_change_report.sql +17 -0
- package/dist/db/migrations/0079_claude_finding_story.sql +16 -0
- package/dist/db/migrations/meta/0000_snapshot.json +1116 -0
- package/dist/db/migrations/meta/0001_snapshot.json +1321 -0
- package/dist/db/migrations/meta/0002_snapshot.json +1375 -0
- package/dist/db/migrations/meta/0003_snapshot.json +1396 -0
- package/dist/db/migrations/meta/0004_snapshot.json +1416 -0
- package/dist/db/migrations/meta/0005_snapshot.json +1430 -0
- package/dist/db/migrations/meta/0006_snapshot.json +1745 -0
- package/dist/db/migrations/meta/0007_snapshot.json +1759 -0
- package/dist/db/migrations/meta/_journal.json +566 -0
- package/dist/db/migrations-pg/0000_tired_alex_power.sql +268 -0
- package/dist/db/migrations-pg/0001_dizzy_mockingbird.sql +3 -0
- package/dist/db/migrations-pg/0002_glorious_ikaris.sql +4 -0
- package/dist/db/migrations-pg/0003_melted_secret_warriors.sql +2 -0
- package/dist/db/migrations-pg/0004_numerous_triton.sql +1 -0
- package/dist/db/migrations-pg/0005_thick_blockbuster.sql +2 -0
- package/dist/db/migrations-pg/0006_majestic_dracula.sql +1 -0
- package/dist/db/migrations-pg/0007_same_human_torch.sql +2 -0
- package/dist/db/migrations-pg/0008_slimy_toxin.sql +1 -0
- package/dist/db/migrations-pg/0009_fuzzy_jubilee.sql +4 -0
- package/dist/db/migrations-pg/0010_late_hammerhead.sql +1 -0
- package/dist/db/migrations-pg/0011_happy_leech.sql +16 -0
- package/dist/db/migrations-pg/0012_woozy_captain_midlands.sql +16 -0
- package/dist/db/migrations-pg/0013_yielding_nicolaos.sql +1 -0
- package/dist/db/migrations-pg/0014_wakeful_bill_hollister.sql +2 -0
- package/dist/db/migrations-pg/0015_aspiring_black_tom.sql +1 -0
- package/dist/db/migrations-pg/0016_cute_ghost_rider.sql +30 -0
- package/dist/db/migrations-pg/0017_far_changeling.sql +24 -0
- package/dist/db/migrations-pg/0018_fancy_miss_america.sql +1 -0
- package/dist/db/migrations-pg/0019_benchmark_contributions.sql +22 -0
- package/dist/db/migrations-pg/0020_majestic_edwin_jarvis.sql +3 -0
- package/dist/db/migrations-pg/0021_green_salo.sql +1 -0
- package/dist/db/migrations-pg/0022_wise_baron_strucker.sql +1 -0
- package/dist/db/migrations-pg/0023_search_index.sql +19 -0
- package/dist/db/migrations-pg/0024_author_indexes.sql +9 -0
- package/dist/db/migrations-pg/0025_auto_merge_requests.sql +26 -0
- package/dist/db/migrations-pg/0026_branch_status.sql +32 -0
- package/dist/db/migrations-pg/0027_pr_review_decision.sql +5 -0
- package/dist/db/migrations-pg/0028_branch_commit_detail.sql +9 -0
- package/dist/db/migrations-pg/0029_repo_reviewers.sql +127 -0
- package/dist/db/migrations-pg/0030_account_reviewers.sql +77 -0
- package/dist/db/migrations-pg/0031_workspaces.sql +131 -0
- package/dist/db/migrations-pg/0032_workspace_reviewers.sql +165 -0
- package/dist/db/migrations-pg/0033_drop_inbox_watch.sql +14 -0
- package/dist/db/migrations-pg/0034_ml_comment_labels.sql +33 -0
- package/dist/db/migrations-pg/0035_ml_vendor_severity.sql +16 -0
- package/dist/db/migrations-pg/0036_seat_pricing_repo_description.sql +12 -0
- package/dist/db/migrations-pg/0037_users_app_slug.sql +9 -0
- package/dist/db/migrations-pg/0038_auto_merge_queue.sql +10 -0
- package/dist/db/migrations-pg/0039_trunk_ci_status_events.sql +27 -0
- package/dist/db/migrations-pg/0040_automation_roles.sql +63 -0
- package/dist/db/migrations-pg/0041_automation_vendor_kinds.sql +488 -0
- package/dist/db/migrations-pg/0042_auto_merge_phase.sql +8 -0
- package/dist/db/migrations-pg/0043_pr_mentions.sql +19 -0
- package/dist/db/migrations-pg/0044_large_pr_threshold.sql +6 -0
- package/dist/db/migrations-pg/0045_pending_mute.sql +26 -0
- package/dist/db/migrations-pg/0046_merge_queue_state.sql +18 -0
- package/dist/db/migrations-pg/0047_drop_my_turn_dismissals.sql +21 -0
- package/dist/db/migrations-pg/0048_auto_merge_durable_anchors.sql +27 -0
- package/dist/db/migrations-pg/0049_blast_radius_config.sql +8 -0
- package/dist/db/migrations-pg/0050_repo_file_coupling.sql +13 -0
- package/dist/db/migrations-pg/0051_pr_content_kind.sql +6 -0
- package/dist/db/migrations-pg/0052_workspace_flow_settings.sql +4 -0
- package/dist/db/migrations-pg/0053_review_request_events.sql +18 -0
- package/dist/db/migrations-pg/0054_pr_security_signals.sql +59 -0
- package/dist/db/migrations-pg/0055_my_turn_settings.sql +6 -0
- package/dist/db/migrations-pg/0056_my_turn_dismissals.sql +21 -0
- package/dist/db/migrations-pg/0057_claude_review_follow_up_ticket.sql +8 -0
- package/dist/db/migrations-pg/0058_claude_review_trigger.sql +4 -0
- package/dist/db/migrations-pg/0059_commit_message_headline.sql +3 -0
- package/dist/db/migrations-pg/0060_claude_review_chat.sql +24 -0
- package/dist/db/migrations-pg/0061_adopt_agentic_tables.sql +79 -0
- package/dist/db/migrations-pg/0062_review_lens_drop_learnings.sql +5 -0
- package/dist/db/migrations-pg/0063_claude_review_threads.sql +7 -0
- package/dist/db/migrations-pg/0064_claude_review_ci_failures.sql +4 -0
- package/dist/db/migrations-pg/0065_ai_fix_change_report.sql +9 -0
- package/dist/db/migrations-pg/0066_claude_finding_story.sql +7 -0
- package/dist/db/migrations-pg/meta/0000_snapshot.json +2137 -0
- package/dist/db/migrations-pg/meta/0001_snapshot.json +2143 -0
- package/dist/db/migrations-pg/meta/0002_snapshot.json +2170 -0
- package/dist/db/migrations-pg/meta/0003_snapshot.json +2182 -0
- package/dist/db/migrations-pg/meta/0004_snapshot.json +2188 -0
- package/dist/db/migrations-pg/meta/0005_snapshot.json +2201 -0
- package/dist/db/migrations-pg/meta/0006_snapshot.json +2207 -0
- package/dist/db/migrations-pg/meta/0007_snapshot.json +2219 -0
- package/dist/db/migrations-pg/meta/0008_snapshot.json +2226 -0
- package/dist/db/migrations-pg/meta/0009_snapshot.json +2250 -0
- package/dist/db/migrations-pg/meta/0010_snapshot.json +2256 -0
- package/dist/db/migrations-pg/meta/0011_snapshot.json +2412 -0
- package/dist/db/migrations-pg/meta/0012_snapshot.json +2528 -0
- package/dist/db/migrations-pg/meta/0013_snapshot.json +2534 -0
- package/dist/db/migrations-pg/meta/0014_snapshot.json +2547 -0
- package/dist/db/migrations-pg/meta/0015_snapshot.json +2553 -0
- package/dist/db/migrations-pg/meta/0016_snapshot.json +2774 -0
- package/dist/db/migrations-pg/meta/0017_snapshot.json +2999 -0
- package/dist/db/migrations-pg/meta/0018_snapshot.json +3020 -0
- package/dist/db/migrations-pg/meta/0020_snapshot.json +3228 -0
- package/dist/db/migrations-pg/meta/0021_snapshot.json +3234 -0
- package/dist/db/migrations-pg/meta/0022_snapshot.json +3240 -0
- package/dist/db/migrations-pg/meta/_journal.json +475 -0
- package/dist/db/ml-labels.js +1666 -0
- package/dist/db/my-turn-dismissals.js +343 -0
- package/dist/db/my-turn-settings.js +34 -0
- package/dist/db/pending-classify.js +46 -0
- package/dist/db/pending-mute.js +171 -0
- package/dist/db/pending-tabs.js +238 -0
- package/dist/db/period-metrics.js +820 -0
- package/dist/db/person-period.js +763 -0
- package/dist/db/pr-intervals.js +930 -0
- package/dist/db/pr-liveness.js +240 -0
- package/dist/db/pr-mentions.js +279 -0
- package/dist/db/pr-merge-queue-stamp.js +91 -0
- package/dist/db/queries.js +14631 -0
- package/dist/db/reaction-targets.js +94 -0
- package/dist/db/repo-activity.js +171 -0
- package/dist/db/retention.js +138 -0
- package/dist/db/review-threads-for-review.js +240 -0
- package/dist/db/run-migrations.js +24 -0
- package/dist/db/schema.pg.js +1220 -0
- package/dist/db/schema.sqlite.js +1801 -0
- package/dist/db/search.js +317 -0
- package/dist/db/security-alerts.js +218 -0
- package/dist/db/synthesis-input.js +565 -0
- package/dist/db/triage.js +320 -0
- package/dist/db/usage.js +63 -0
- package/dist/db/work-plan.js +1086 -0
- package/dist/db/working-hours.js +161 -0
- package/dist/feed/my-turn.js +122 -0
- package/dist/github/actions-logs.js +397 -0
- package/dist/github/auth.js +86 -0
- package/dist/github/branch-queries.js +229 -0
- package/dist/github/client.js +454 -0
- package/dist/github/codeowners.js +221 -0
- package/dist/github/commit-checks.js +105 -0
- package/dist/github/compare.js +110 -0
- package/dist/github/diff-anchor.js +227 -0
- package/dist/github/mutations.js +706 -0
- package/dist/github/pr-file-content.js +254 -0
- package/dist/github/pr-liveness.js +161 -0
- package/dist/github/queries.js +875 -0
- package/dist/github/rate-budget.js +129 -0
- package/dist/github/reactions.js +161 -0
- package/dist/github/reviewer-suggest.js +104 -0
- package/dist/github/team-reviewers.js +96 -0
- package/dist/index.js +184 -0
- package/dist/merge/auto-merge-runner.js +960 -0
- package/dist/ml/severity-client.js +218 -0
- package/dist/pr/detail-enricher.js +26 -0
- package/dist/pro/bind.js +302 -0
- package/dist/pro/contract.js +37 -0
- package/dist/pro/migrate.js +64 -0
- package/dist/review/agent-context.js +127 -0
- package/dist/review/agent.js +385 -0
- package/dist/review/agentic.js +59 -0
- package/dist/review/auth.js +94 -0
- package/dist/review/chat-agent.js +179 -0
- package/dist/review/claude-review/ai-ready.js +16 -0
- package/dist/review/claude-review/auto-settings.js +139 -0
- package/dist/review/claude-review/auto.js +244 -0
- package/dist/review/claude-review/chat.js +489 -0
- package/dist/review/claude-review/ci-failures.js +350 -0
- package/dist/review/claude-review/follow-up.js +310 -0
- package/dist/review/claude-review/manager.js +686 -0
- package/dist/review/claude-review/persist.js +996 -0
- package/dist/review/claude-review/prompts.js +357 -0
- package/dist/review/claude-review/routes.js +518 -0
- package/dist/review/claude-review/routing.js +61 -0
- package/dist/review/claude-review/specialists.js +190 -0
- package/dist/review/claude-review/threads.js +265 -0
- package/dist/review/claude-review/ticket.js +282 -0
- package/dist/review/clone-hygiene.js +380 -0
- package/dist/review/clone-manager.js +367 -0
- package/dist/review/llm.js +113 -0
- package/dist/review/model-options.js +48 -0
- package/dist/review/plugin-providers.js +12 -0
- package/dist/review/post-review.js +216 -0
- package/dist/review/post-seam.js +161 -0
- package/dist/review/prepare.js +168 -0
- package/dist/review/pricing.js +17 -0
- package/dist/review/schema.js +156 -0
- package/dist/review/submit-map.js +41 -0
- package/dist/review/usage.js +90 -0
- package/dist/shared/claude-review.js +335 -0
- package/dist/shared/conflict-fold.js +151 -0
- package/dist/shared/conflicts.js +30 -0
- package/dist/shared/flow-settings.js +191 -0
- package/dist/shared/index.js +8 -0
- package/dist/shared/my-turn-settings.js +238 -0
- package/dist/shared/pending-rules.js +281 -0
- package/dist/shared/product.js +54 -0
- package/dist/shared/types.js +896 -0
- package/dist/status.js +386 -0
- package/dist/sync/adaptive.js +185 -0
- package/dist/sync/app-attribution.js +66 -0
- package/dist/sync/auth-notices.js +36 -0
- package/dist/sync/backfill-ci-history.js +315 -0
- package/dist/sync/backfill-commit-headlines.js +136 -0
- package/dist/sync/backfill-pr-security.js +175 -0
- package/dist/sync/backfill-review-requests.js +97 -0
- package/dist/sync/benchmark-rollup.js +50 -0
- package/dist/sync/bot-detection.js +309 -0
- package/dist/sync/bot-resolution-markers.js +30 -0
- package/dist/sync/branch-status.js +634 -0
- package/dist/sync/classify-change-shape.js +150 -0
- package/dist/sync/commit-files.js +74 -0
- package/dist/sync/derive-thread-state.js +99 -0
- package/dist/sync/drain-review-threads.js +212 -0
- package/dist/sync/hydrate-detail.js +482 -0
- package/dist/sync/mention-scan.js +89 -0
- package/dist/sync/ml-enrichment.js +478 -0
- package/dist/sync/pr-change-signal.js +96 -0
- package/dist/sync/pr-liveness-sweep.js +139 -0
- package/dist/sync/pr-settle.js +381 -0
- package/dist/sync/refresh-pr.js +163 -0
- package/dist/sync/reparse-vendor-badges.js +495 -0
- package/dist/sync/resync-after-write.js +280 -0
- package/dist/sync/review-fingerprint.js +117 -0
- package/dist/sync/reviewer-behavior.js +103 -0
- package/dist/sync/reviewer-classify.js +503 -0
- package/dist/sync/routing-files.js +97 -0
- package/dist/sync/scheduled-jobs.js +12 -0
- package/dist/sync/scheduler.js +159 -0
- package/dist/sync/security-detect.js +521 -0
- package/dist/sync/sync-manager.js +750 -0
- package/dist/sync/sync-one-pr.js +205 -0
- package/dist/sync/sync-repo.js +400 -0
- package/dist/sync/unsettled-prs.js +506 -0
- package/dist/sync/upsert.js +1190 -0
- package/package.json +56 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/assets/AiFixTab-CQP6PT5u.js +2 -0
- package/public/assets/ClaudeReviewTab-DQJ-58ZA.js +8 -0
- package/public/assets/PrBotBehaviourTab-CCnFrWbC.js +1 -0
- package/public/assets/archivo-latin-600-BN-JkNrl.woff2 +0 -0
- package/public/assets/highlight-BEHUn5zE.css +10 -0
- package/public/assets/highlight-CIIZLa6Z.js +12 -0
- package/public/assets/index-BDnYs6zg.js +73 -0
- package/public/assets/index-mCuI--RC.css +1 -0
- package/public/assets/markdown-I_JLLVAZ.js +32 -0
- package/public/assets/perfProbe-Da13EhTE.js +1 -0
- package/public/assets/tanstack-5qsxxaLe.js +17 -0
- package/public/assets/vis-C3BCkP5U.js +1280 -0
- package/public/assets/vis-ziCVehMy.css +1 -0
- package/public/favicon-16.png +0 -0
- package/public/favicon-32.png +0 -0
- package/public/favicon-48.png +0 -0
- package/public/favicon.svg +5 -0
- package/public/index.html +41 -0
- package/public-landing/apple-touch-icon.png +0 -0
- package/public-landing/assets/archivo-latin-500-BWmvx2Mg.woff2 +0 -0
- package/public-landing/assets/archivo-latin-600-BN-JkNrl.woff2 +0 -0
- package/public-landing/assets/index-CA3eEGXO.js +40 -0
- package/public-landing/assets/index-naQrQ6Fe.css +1 -0
- package/public-landing/assets/jetbrains-mono-latin-400-RruCX7RW.woff2 +0 -0
- package/public-landing/assets/newsreader-latin-400-DExrRRrN.woff2 +0 -0
- package/public-landing/assets/newsreader-latin-400-italic-b0I-AwbV.woff2 +0 -0
- package/public-landing/bots/index.html +156 -0
- package/public-landing/contact/index.html +156 -0
- package/public-landing/cookies/index.html +156 -0
- package/public-landing/demo/limn-walkthrough-poster.jpg +0 -0
- package/public-landing/demo/limn-walkthrough.cues.json +44 -0
- package/public-landing/demo/limn-walkthrough.mp4 +0 -0
- package/public-landing/favicon-16.png +0 -0
- package/public-landing/favicon-32.png +0 -0
- package/public-landing/favicon-48.png +0 -0
- package/public-landing/favicon.svg +5 -0
- package/public-landing/features/index.html +156 -0
- package/public-landing/for-developers/index.html +156 -0
- package/public-landing/for-managers/index.html +156 -0
- package/public-landing/how-it-works/index.html +156 -0
- package/public-landing/how-we-measure/index.html +156 -0
- package/public-landing/icon-192.png +0 -0
- package/public-landing/icon-512.png +0 -0
- package/public-landing/index.html +156 -0
- package/public-landing/insights/index.html +156 -0
- package/public-landing/og-image.png +0 -0
- package/public-landing/pricing/index.html +156 -0
- package/public-landing/privacy/index.html +156 -0
- package/public-landing/pro/index.html +156 -0
- package/public-landing/reviews/index.html +156 -0
- package/public-landing/robots.txt +8 -0
- package/public-landing/shots/benchmark.png +0 -0
- package/public-landing/shots/bot-roi.png +0 -0
- package/public-landing/shots/bot-settings.png +0 -0
- package/public-landing/shots/chronology.png +0 -0
- package/public-landing/shots/feed.png +0 -0
- package/public-landing/shots/flow-metrics.png +0 -0
- package/public-landing/shots/free-reports.png +0 -0
- package/public-landing/shots/pending-board.png +0 -0
- package/public-landing/shots/pending-card.png +0 -0
- package/public-landing/shots/period-report.png +0 -0
- package/public-landing/shots/pr-detail.png +0 -0
- package/public-landing/shots/pr-threads.png +0 -0
- package/public-landing/shots/reach.png +0 -0
- package/public-landing/shots/repo-rows.png +0 -0
- package/public-landing/shots/severity-findings.png +0 -0
- package/public-landing/shots/severity-strip.png +0 -0
- package/public-landing/site.webmanifest +14 -0
- package/public-landing/sitemap.xml +51 -0
- package/public-landing/terms/index.html +156 -0
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
// ── Bot comment VOLUME per PR (CORE, free, deterministic — no model, no new table) ──────────
|
|
2
|
+
//
|
|
3
|
+
// Answers two questions the ROI tab could not: "how much does this bot say on a PR" and "does it
|
|
4
|
+
// scale with the diff". Three getters over ONE shared base scan:
|
|
5
|
+
// getBotVolume → the per-bot column + workspace totals (Bots ROI tab)
|
|
6
|
+
// getPrBotVolume → the paginated PR drill-down behind that column
|
|
7
|
+
// getBotVolumeScatter → the LOC-vs-volume chart series (Bots BEHAVIOUR tab)
|
|
8
|
+
//
|
|
9
|
+
// ⚠ ALL THREE FOLD THE SAME `loadVolumeBase` RESULT. That is the point: a column, the list behind
|
|
10
|
+
// it and the chart beside it are three renderings of one count, so they cannot drift the way a
|
|
11
|
+
// tile and its drill-down drift when each runs its own scan (the failure db/bot-flagging.test.ts
|
|
12
|
+
// exists to prevent). Nothing client-side re-derives a count from items — the server's number is
|
|
13
|
+
// the number.
|
|
14
|
+
//
|
|
15
|
+
// ⚠ THE POPULATION IS **MERGED** PRs AND THE WINDOW APPLIES TO `pullRequests.mergedAt`. An open PR
|
|
16
|
+
// has not finished collecting bot comments, so including it drags every mean down by an amount
|
|
17
|
+
// that depends only on how recently the window opened; and `mergedAt` is already the anchor
|
|
18
|
+
// `BotVendorAnalytics.mergedPastPrs` uses, so the ROI tab keeps ONE time grain across its columns.
|
|
19
|
+
// Measured at 180d on the dev corpus, erxes/erxes holds 997 PRs by `openedAt` and 686 by
|
|
20
|
+
// `mergedAt` — the choice moves every number materially and every caption must state it.
|
|
21
|
+
//
|
|
22
|
+
// ⚠ SIZE IS SUBLINEAR IN COMMENTS, WHICH IS WHY `expected` EXISTS. Measured (erxes, 180d, mean
|
|
23
|
+
// bot comments per PR by bucket): 6.80 → 9.49 → 16.18 → 23.45 → 35.32 across <50 / 50-200 /
|
|
24
|
+
// 200-600 / 600-2k / 2k+ LOC, i.e. comments per 100 LOC FALLING monotonically — 32.91 → 8.33 →
|
|
25
|
+
// 4.32 → 2.08 → 0.30 aggregated (Σcomments/Σloc, the form `BotVolumeSizeBucketStat` returns), or
|
|
26
|
+
// 57.65 → 8.99 → 4.46 → 2.23 → 0.83 as a mean of per-PR densities. go-redis says the same thing
|
|
27
|
+
// louder: 0.32 → 2.08 → 3.00 → 13.00 → 28.40. So ranking PRs by raw comment count mostly ranks
|
|
28
|
+
// them BY SIZE (correlation of log10(LOC+1) against count: go-redis 0.615, erxes 0.539 — and
|
|
29
|
+
// ~nothing where bots are quiet: three.js 0.125).
|
|
30
|
+
//
|
|
31
|
+
// The product question is "where did a bot tear a PR apart", and the answer is the BUCKET-RELATIVE
|
|
32
|
+
// ratio. THE PROOF, on this corpus: erxes #7802 is 17 LOC across 1 file and drew 25 bot comments —
|
|
33
|
+
// 3.68× its bucket's expectation of 6.80. Under the default `sort=comments` it ranks **123rd of
|
|
34
|
+
// 686**, below every large PR; under `sort=ratio` it ranks **8th**. That is the entire reason the
|
|
35
|
+
// second sort exists, and why a UI that only ever shows the first has shipped a size ranking.
|
|
36
|
+
//
|
|
37
|
+
// ⚠ AND `expected` IS NOT A DENSITY. `comments per 100 LOC` was the obvious first form and it is
|
|
38
|
+
// unusable as a ranking: it explodes on tiny PRs by construction (#7802's is 147.06). It ships as
|
|
39
|
+
// `commentsPer100Loc`, never as the sort, and the aggregated per-bucket version is the only place
|
|
40
|
+
// it reads honestly.
|
|
41
|
+
//
|
|
42
|
+
// ⚠ KNOWN LIMIT OF THE RATIO — A NEAR-ZERO EXPECTATION INFLATES IT. In a repo where bots are
|
|
43
|
+
// almost silent the bucket mean can be a fraction, and a PR with 3 comments then reads 42.86×
|
|
44
|
+
// (measured: bevyengine/bevy #24971, 3 comments against an expectation of 0.07 over 61 PRs). That
|
|
45
|
+
// is arithmetically correct and genuinely notable, but it is not the same claim as erxes #7802 and
|
|
46
|
+
// must not be presented as one — which is why `expected` and `baselinePrs` ride every row and are
|
|
47
|
+
// meant to be rendered BESIDE the multiplier, never behind a tooltip. The floor below fixes small
|
|
48
|
+
// SAMPLES; nothing can fix a small MEAN except showing it.
|
|
49
|
+
//
|
|
50
|
+
// ⚑ ITS OWN FILE, NOT queries.ts: that file is 13k lines and CONTAINS LITERAL NUL BYTES around
|
|
51
|
+
// offset 132k, so every search tool silently under-reports matches inside it. Nothing imports
|
|
52
|
+
// this file back, so importing queries.ts here creates no cycle (the db/bot-overlap.ts
|
|
53
|
+
// precedent).
|
|
54
|
+
//
|
|
55
|
+
// Nothing here may feed `botVerdict` — display only (bot-analytics-verdict.test.ts pins that
|
|
56
|
+
// verdict's inputs).
|
|
57
|
+
import { and, count, desc, eq, gte, inArray, lte, ne } from 'drizzle-orm';
|
|
58
|
+
import { db, schema } from './client.js';
|
|
59
|
+
import { botWindowMs } from './bot-window.js';
|
|
60
|
+
import { labelFor as labelForKind } from '../sync/reviewer-classify.js';
|
|
61
|
+
import { automatedReviewerUserIds, classificationKindForUser, classificationLabelMap, reviewerRoleForUser, } from './queries.js';
|
|
62
|
+
const { prComments, pullRequests, repos, reviewComments, reviews, users } = schema;
|
|
63
|
+
// The merged-PR scan bound. Generous because a PR row is small and the fold is O(n): the busiest
|
|
64
|
+
// repo in the dev corpus contributes 997 merged PRs over 180 days, so a workspace has to be very
|
|
65
|
+
// large before this bites. Newest-merged-first, and `truncated` says so when it does — the
|
|
66
|
+
// ROLLUP_SCAN_CAP honesty rule (a capped scan reports a most-recent sample, never a total).
|
|
67
|
+
const VOLUME_PR_SCAN_CAP = 5000;
|
|
68
|
+
// The chart's own, tighter bound. This is a scatter, not a listing: 2000 points already paint a
|
|
69
|
+
// solid cloud, and each one is ~60 bytes of JSON. Set independently of the scan cap so the UI can
|
|
70
|
+
// say "the most recent 2000" without implying the aggregates were sampled too.
|
|
71
|
+
const SCATTER_POINT_CAP = 2000;
|
|
72
|
+
// Drill-down page bound (the flagging route's FLAGGING_PAGE_MAX precedent).
|
|
73
|
+
const VOLUME_PAGE_MAX = 50;
|
|
74
|
+
// ── THE SMALL-SAMPLE FLOOR ──────────────────────────────────────────────────────────────────
|
|
75
|
+
// A (repo × bucket) cell needs at least this many SIZED PRs before its mean may be called an
|
|
76
|
+
// expectation. Below it the cell degrades to the repo mean, and a repo under the floor answers
|
|
77
|
+
// no baseline at all. "3.7× expectation" computed off two PRs is noise dressed as a finding, and
|
|
78
|
+
// it is exactly the kind of number a user acts on — so the wire carries `baseline` and
|
|
79
|
+
// `baselinePrs` and never leaves the UI to infer sample size from a null.
|
|
80
|
+
const BASELINE_MIN_PRS = 5;
|
|
81
|
+
// ── THE EXPECTATION FLOOR (a SEPARATE guard from BASELINE_MIN_PRS above) ────────────────────
|
|
82
|
+
// A cell may hold plenty of PRs and still be useless as a comparison, because the floor that
|
|
83
|
+
// matters is the EXPECTED COUNT, not the sample size. Measured on erxes/30d: the `<50` cell held
|
|
84
|
+
// 43 merged PRs — comfortably past BASELINE_MIN_PRS — at a mean of 0.9 comments. A single PR
|
|
85
|
+
// drawing 4 comments there reads 4.4×, and four of the top eleven "most vs expected" rows were
|
|
86
|
+
// 3-4 raw comments against that 0.9. With λ≈0.9, P(X≥4) ≈ 1.3% per PR, so across 43 PRs you
|
|
87
|
+
// EXPECT about one such row from Poisson noise alone — indistinguishable, in the same list, from
|
|
88
|
+
// erxes #9013 (36 comments against an expectation of 6.2), which is a real finding.
|
|
89
|
+
//
|
|
90
|
+
// So below this floor the row reports its `expected` and `baselinePrs` but NO ratio: there is
|
|
91
|
+
// nothing here a multiplier can honestly say. Suppressing the number is the point — a greyed
|
|
92
|
+
// "—" that explains itself beats a confident 4.4× that is noise.
|
|
93
|
+
const BASELINE_MIN_EXPECTED = 3;
|
|
94
|
+
// ── THE BASELINE SPAN ───────────────────────────────────────────────────────────────────────
|
|
95
|
+
// The expectation is ALWAYS computed over this many days, regardless of the window being VIEWED.
|
|
96
|
+
// Two reasons, both learned from the 30d screenshots:
|
|
97
|
+
// 1. STABILITY. A baseline tied to the display window moves under the reader — the same PR
|
|
98
|
+
// reads 3.3× at 30d and something else at 7d, with nothing on screen having changed about
|
|
99
|
+
// the PR. A comparison that shifts when you change the view is not a comparison.
|
|
100
|
+
// 2. SAMPLE. At 7d most (repo × bucket) cells fall under BASELINE_MIN_PRS and degrade to the
|
|
101
|
+
// repo mean or to no baseline at all, so the column empties out exactly when the user
|
|
102
|
+
// narrows to look closely.
|
|
103
|
+
// The LIST is still the selected window; only what it is COMPARED AGAINST is fixed. When the
|
|
104
|
+
// selected window is already longer, it wins (`min(from, to − 90d)`) — never a narrower baseline
|
|
105
|
+
// than the rows it judges.
|
|
106
|
+
const BASELINE_DAYS = 90;
|
|
107
|
+
const BASELINE_SPAN_MS = BASELINE_DAYS * 86_400_000;
|
|
108
|
+
// The ONE runtime table of LOC bucket edges. Half-open [minLoc, maxLoc); `maxLoc: null` is the
|
|
109
|
+
// open-ended top. These are the buckets the corpus measurement was taken on — changing an edge
|
|
110
|
+
// invalidates every measured figure quoted in the comments above and in the shared types.
|
|
111
|
+
// Kept HERE and shipped on the wire (BotVolumeSizeBucketStat.minLoc/maxLoc + `label`) rather than
|
|
112
|
+
// duplicated into the SPA: `packages/shared` is types-only, so a client copy could only be a
|
|
113
|
+
// second spelling waiting to drift.
|
|
114
|
+
const SIZE_BUCKETS = [
|
|
115
|
+
{ bucket: 'xs', label: '<50', minLoc: 0, maxLoc: 50 },
|
|
116
|
+
{ bucket: 's', label: '50–200', minLoc: 50, maxLoc: 200 },
|
|
117
|
+
{ bucket: 'm', label: '200–600', minLoc: 200, maxLoc: 600 },
|
|
118
|
+
{ bucket: 'l', label: '600–2k', minLoc: 600, maxLoc: 2000 },
|
|
119
|
+
{ bucket: 'xl', label: '2k+', minLoc: 2000, maxLoc: null },
|
|
120
|
+
];
|
|
121
|
+
function bucketFor(loc) {
|
|
122
|
+
for (const b of SIZE_BUCKETS) {
|
|
123
|
+
if (b.maxLoc == null || loc < b.maxLoc)
|
|
124
|
+
return b.bucket;
|
|
125
|
+
}
|
|
126
|
+
// Unreachable — the last bucket is open-ended. Non-null return keeps the caller honest.
|
|
127
|
+
return 'xl';
|
|
128
|
+
}
|
|
129
|
+
/** 2dp, or null. Money-style rounding is deliberate: these are display figures, and an
|
|
130
|
+
* unrounded 16.888888888888889 in a table cell is worse than useless. */
|
|
131
|
+
function round2(n) {
|
|
132
|
+
return Math.round(n * 100) / 100;
|
|
133
|
+
}
|
|
134
|
+
function avg2(sum, n) {
|
|
135
|
+
return n > 0 ? round2(sum / n) : null;
|
|
136
|
+
}
|
|
137
|
+
function emptyBase(windowKind, from, to, generatedAt) {
|
|
138
|
+
return {
|
|
139
|
+
windowKind,
|
|
140
|
+
from,
|
|
141
|
+
to,
|
|
142
|
+
generatedAt,
|
|
143
|
+
prs: [],
|
|
144
|
+
truncated: false,
|
|
145
|
+
automatedIds: [],
|
|
146
|
+
repoFullName: new Map(),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* The merged-PR population plus its per-(PR, bot) comment counts.
|
|
151
|
+
*
|
|
152
|
+
* ⚠ THE BOT SET IS ROLE `'all'`, NOT `'review'` — the ROI / flagging convention. Quality checks
|
|
153
|
+
* (SonarQube, Codecov …) post exactly the kind of text this counts, and a drill-down that
|
|
154
|
+
* narrowed the bot set would list fewer comments than the column it was opened from. The response
|
|
155
|
+
* carries each bot's `role` so the UI can split them the way `BotAnalyticsResponse` splits
|
|
156
|
+
* `vendors` from `qualityChecks`; it does NOT pre-split, because the chart wants the whole cloud.
|
|
157
|
+
* (The Behaviour tab's own panels use `'review'` — this file deliberately does not follow them.)
|
|
158
|
+
*/
|
|
159
|
+
async function loadVolumeBase(accountId, window, scope) {
|
|
160
|
+
const nowMs = Date.now();
|
|
161
|
+
const windowKind = typeof window === 'string' ? window : window.kind;
|
|
162
|
+
// The one shared window→duration mapping (db/bot-window.ts), unless the caller supplied real
|
|
163
|
+
// bounds — the `getBotAnalytics` window contract verbatim, so the Pro Insights chat's ranges
|
|
164
|
+
// and a verification script can both reach a span the enum cannot spell.
|
|
165
|
+
const to = new Date(typeof window === 'string' ? nowMs : window.toMs);
|
|
166
|
+
const from = new Date(typeof window === 'string' ? nowMs - botWindowMs(window) : window.fromMs);
|
|
167
|
+
const generatedAt = new Date(nowMs).toISOString();
|
|
168
|
+
// The SCAN reaches back to whichever is earlier: the window's own start, or BASELINE_DAYS
|
|
169
|
+
// before its end. Everything loaded feeds the expectation; only `inWindow` rows are reported.
|
|
170
|
+
// One scan, not two — a second query for the baseline would double the cost of every call to
|
|
171
|
+
// buy nothing, since the wider span is a strict superset.
|
|
172
|
+
const scanFrom = new Date(Math.min(from.getTime(), to.getTime() - BASELINE_SPAN_MS));
|
|
173
|
+
// `repoIds: []` is a real empty workspace, never "widen to the account".
|
|
174
|
+
if (scope.repoIds.length === 0)
|
|
175
|
+
return emptyBase(windowKind, from, to, generatedAt);
|
|
176
|
+
const automatedIds = await automatedReviewerUserIds(accountId, scope.workspaceId, 'all');
|
|
177
|
+
if (automatedIds.length === 0) {
|
|
178
|
+
// No bots ⇒ no comments, but the PR population is still real and `totals.prs` must report it
|
|
179
|
+
// (a workspace with 800 merged PRs and no bots reads "0 of 800", not "no data").
|
|
180
|
+
const bare = await loadMergedPrs(accountId, scope, scanFrom, to, from);
|
|
181
|
+
return { windowKind, from, to, generatedAt, ...bare, automatedIds: [] };
|
|
182
|
+
}
|
|
183
|
+
const bare = await loadMergedPrs(accountId, scope, scanFrom, to, from);
|
|
184
|
+
const byId = new Map(bare.prs.map((p) => [p.id, p]));
|
|
185
|
+
// The three text kinds, one grouped count each. Deliberately NOT one clever aggregate:
|
|
186
|
+
// `count(*) FILTER (WHERE …)` is not portable across the two dialects (the listDetectedReviewers
|
|
187
|
+
// precedent), and three indexed group-bys are cheap.
|
|
188
|
+
//
|
|
189
|
+
// ⚠ EACH RE-APPLIES THE FULL WINDOW + SCOPE PREDICATE against `pullRequests` rather than an
|
|
190
|
+
// `inArray` over the scanned PR ids: the id list can reach VOLUME_PR_SCAN_CAP, which is far past
|
|
191
|
+
// sqlite's bound-parameter ceiling. Rows whose PR fell outside the (capped) scan simply find no
|
|
192
|
+
// entry in `byId` and are dropped.
|
|
193
|
+
const prPred = and(eq(pullRequests.accountId, accountId), inArray(pullRequests.repoId, scope.repoIds), gte(pullRequests.mergedAt, scanFrom), lte(pullRequests.mergedAt, to));
|
|
194
|
+
const [rcRows, pcRows, rvRows] = await Promise.all([
|
|
195
|
+
// Inline review comments. Joined on `reviewComments.prId` — provably identical to routing
|
|
196
|
+
// through `reviewThreads.prId` (sync writes both from the same thread, and 0 of 21,475 rows
|
|
197
|
+
// in the dev corpus disagree), and `rc_pr_node` makes `pr_id` a leading indexed column while
|
|
198
|
+
// the thread hop would be an extra join per row.
|
|
199
|
+
db
|
|
200
|
+
.select({ prId: reviewComments.prId, authorId: reviewComments.authorId, c: count() })
|
|
201
|
+
.from(reviewComments)
|
|
202
|
+
.innerJoin(pullRequests, eq(pullRequests.id, reviewComments.prId))
|
|
203
|
+
.where(and(prPred, inArray(reviewComments.authorId, automatedIds)))
|
|
204
|
+
.groupBy(reviewComments.prId, reviewComments.authorId)
|
|
205
|
+
.execute(),
|
|
206
|
+
db
|
|
207
|
+
.select({ prId: prComments.prId, authorId: prComments.authorId, c: count() })
|
|
208
|
+
.from(prComments)
|
|
209
|
+
.innerJoin(pullRequests, eq(pullRequests.id, prComments.prId))
|
|
210
|
+
.where(and(prPred, inArray(prComments.authorId, automatedIds)))
|
|
211
|
+
.groupBy(prComments.prId, prComments.authorId)
|
|
212
|
+
.execute(),
|
|
213
|
+
// Submitted review BODIES (the walkthrough / summary post).
|
|
214
|
+
//
|
|
215
|
+
// ⚠ A BODILESS REVIEW COUNTS. A bare approval with no text is one row here, and that is the
|
|
216
|
+
// definition the corpus measurement these surfaces were specced against used. The measured
|
|
217
|
+
// cost of the choice: erxes/erxes 16.89 vs 15.98 avg, go-redis 4.91 vs 4.32 — and the
|
|
218
|
+
// zero-comment PR counts are IDENTICAL under both, so no product signal turns on it. To flip
|
|
219
|
+
// it, add a non-empty-body predicate HERE and nowhere else, and restate the numbers quoted at
|
|
220
|
+
// the top of this file.
|
|
221
|
+
//
|
|
222
|
+
// `pending` reviews are drafts, invisible on GitHub, and are excluded — the same predicate
|
|
223
|
+
// getBotAnalytics uses for review activity.
|
|
224
|
+
db
|
|
225
|
+
.select({ prId: reviews.prId, authorId: reviews.authorId, c: count() })
|
|
226
|
+
.from(reviews)
|
|
227
|
+
.innerJoin(pullRequests, eq(pullRequests.id, reviews.prId))
|
|
228
|
+
.where(and(prPred, inArray(reviews.authorId, automatedIds), ne(reviews.state, 'pending')))
|
|
229
|
+
.groupBy(reviews.prId, reviews.authorId)
|
|
230
|
+
.execute(),
|
|
231
|
+
]);
|
|
232
|
+
for (const rows of [rcRows, pcRows, rvRows]) {
|
|
233
|
+
for (const r of rows) {
|
|
234
|
+
if (r.authorId == null)
|
|
235
|
+
continue;
|
|
236
|
+
const pr = byId.get(r.prId);
|
|
237
|
+
if (!pr)
|
|
238
|
+
continue; // outside the capped scan
|
|
239
|
+
pr.byBot.set(r.authorId, (pr.byBot.get(r.authorId) ?? 0) + r.c);
|
|
240
|
+
pr.total += r.c;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return { windowKind, from, to, generatedAt, ...bare, automatedIds };
|
|
244
|
+
}
|
|
245
|
+
/** The PR half of the base — split out so the no-bots early return can reuse it verbatim. */
|
|
246
|
+
async function loadMergedPrs(accountId, scope,
|
|
247
|
+
// The SCAN span — reaches back to the baseline horizon, which is at or before `windowFrom`.
|
|
248
|
+
from, to,
|
|
249
|
+
// The SELECTED window's start. Rows merged before it are loaded to condition the expectation
|
|
250
|
+
// and are flagged `inWindow: false`; every user-facing fold must drop them.
|
|
251
|
+
windowFrom) {
|
|
252
|
+
const rows = await db
|
|
253
|
+
.select({
|
|
254
|
+
id: pullRequests.id,
|
|
255
|
+
repoId: pullRequests.repoId,
|
|
256
|
+
number: pullRequests.number,
|
|
257
|
+
title: pullRequests.title,
|
|
258
|
+
openedAt: pullRequests.openedAt,
|
|
259
|
+
mergedAt: pullRequests.mergedAt,
|
|
260
|
+
additions: pullRequests.additions,
|
|
261
|
+
deletions: pullRequests.deletions,
|
|
262
|
+
changedFiles: pullRequests.changedFiles,
|
|
263
|
+
owner: repos.owner,
|
|
264
|
+
name: repos.name,
|
|
265
|
+
})
|
|
266
|
+
.from(pullRequests)
|
|
267
|
+
.innerJoin(repos, eq(repos.id, pullRequests.repoId))
|
|
268
|
+
.where(and(eq(pullRequests.accountId, accountId), inArray(pullRequests.repoId, scope.repoIds), gte(pullRequests.mergedAt, from), lte(pullRequests.mergedAt, to)))
|
|
269
|
+
// Newest-merged first: both caps slice from the front, so a truncated scan is a RECENT
|
|
270
|
+
// sample rather than an arbitrary one. `pullRequests.id` breaks ties so the cap is
|
|
271
|
+
// deterministic across requests (two PRs can share a merge second).
|
|
272
|
+
.orderBy(desc(pullRequests.mergedAt), desc(pullRequests.id))
|
|
273
|
+
.limit(VOLUME_PR_SCAN_CAP)
|
|
274
|
+
.execute();
|
|
275
|
+
const repoFullName = new Map();
|
|
276
|
+
const prs = [];
|
|
277
|
+
for (const r of rows) {
|
|
278
|
+
// `mergedAt` cannot be null here (the gte/lte predicate excludes nulls in both dialects), but
|
|
279
|
+
// the column IS nullable, so the narrowing is explicit rather than a `!`.
|
|
280
|
+
if (!r.mergedAt)
|
|
281
|
+
continue;
|
|
282
|
+
repoFullName.set(r.repoId, `${r.owner}/${r.name}`);
|
|
283
|
+
// ⚠ UNKNOWN SIZE, NOT ZERO SIZE. Under lean storage these three columns default to 0, so a PR
|
|
284
|
+
// whose detail never hydrated looks exactly like an empty one. 135 of three.js's 796 merged
|
|
285
|
+
// PRs are in this state in the dev corpus — fabricating a 0 would drop every one of them into
|
|
286
|
+
// the `xs` bucket, where the smallest expectation lives, and manufacture spectacular ratios.
|
|
287
|
+
const observed = r.changedFiles > 0 || r.additions > 0 || r.deletions > 0;
|
|
288
|
+
const loc = observed ? r.additions + r.deletions : null;
|
|
289
|
+
prs.push({
|
|
290
|
+
id: r.id,
|
|
291
|
+
repoId: r.repoId,
|
|
292
|
+
number: r.number,
|
|
293
|
+
title: r.title,
|
|
294
|
+
openedAt: r.openedAt,
|
|
295
|
+
mergedAt: r.mergedAt,
|
|
296
|
+
additions: r.additions,
|
|
297
|
+
deletions: r.deletions,
|
|
298
|
+
changedFiles: r.changedFiles,
|
|
299
|
+
loc,
|
|
300
|
+
bucket: loc == null ? null : bucketFor(loc),
|
|
301
|
+
inWindow: r.mergedAt.getTime() >= windowFrom.getTime(),
|
|
302
|
+
byBot: new Map(),
|
|
303
|
+
total: 0,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
return { prs, truncated: rows.length >= VOLUME_PR_SCAN_CAP, repoFullName };
|
|
307
|
+
}
|
|
308
|
+
export async function loadBotIdentities(accountId, scope, automatedIds) {
|
|
309
|
+
const out = new Map();
|
|
310
|
+
if (automatedIds.length === 0)
|
|
311
|
+
return out;
|
|
312
|
+
const [kindMap, roleMap, classLabel, userRows] = await Promise.all([
|
|
313
|
+
classificationKindForUser(accountId, scope.workspaceId),
|
|
314
|
+
reviewerRoleForUser(accountId, scope.workspaceId),
|
|
315
|
+
classificationLabelMap(accountId, scope.workspaceId),
|
|
316
|
+
db
|
|
317
|
+
.select({ id: users.id, login: users.githubLogin, name: users.displayName })
|
|
318
|
+
.from(users)
|
|
319
|
+
.where(inArray(users.id, automatedIds))
|
|
320
|
+
.execute(),
|
|
321
|
+
]);
|
|
322
|
+
const display = new Map();
|
|
323
|
+
const login = new Map();
|
|
324
|
+
for (const u of userRows) {
|
|
325
|
+
display.set(u.id, u.name?.trim() || u.login || `#${u.id}`);
|
|
326
|
+
login.set(u.id, u.login || null);
|
|
327
|
+
}
|
|
328
|
+
for (const id of automatedIds) {
|
|
329
|
+
const kind = kindMap.get(id) ?? 'in_house';
|
|
330
|
+
// getBotAnalytics' `reviewerLabel` precedence verbatim — custom workspace label → vendor
|
|
331
|
+
// pretty name → display name/login — so one bot reads with one name across the Bots interface.
|
|
332
|
+
const custom = classLabel.get(id);
|
|
333
|
+
const label = custom ??
|
|
334
|
+
(kind !== 'in_house' && kind !== 'pierre' && kind !== 'vendor'
|
|
335
|
+
? labelForKind(kind)
|
|
336
|
+
: (display.get(id) ?? labelForKind(kind)));
|
|
337
|
+
out.set(id, {
|
|
338
|
+
label,
|
|
339
|
+
login: login.get(id) ?? null,
|
|
340
|
+
kind,
|
|
341
|
+
role: roleMap.get(id) ?? 'review',
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
return out;
|
|
345
|
+
}
|
|
346
|
+
// ── (1) The per-bot column + workspace totals ───────────────────────────────────────────────
|
|
347
|
+
export async function getBotVolume(accountId, window, scope) {
|
|
348
|
+
const base = await loadVolumeBase(accountId, window, scope);
|
|
349
|
+
const win = {
|
|
350
|
+
kind: base.windowKind,
|
|
351
|
+
from: base.from.toISOString(),
|
|
352
|
+
to: base.to.toISOString(),
|
|
353
|
+
};
|
|
354
|
+
const byBot = new Map();
|
|
355
|
+
let comments = 0;
|
|
356
|
+
let sizedPrs = 0;
|
|
357
|
+
let prsWithBotComments = 0;
|
|
358
|
+
let maxOnOnePr = 0;
|
|
359
|
+
// ⚠ WINDOW ONLY. `base.prs` reaches back to the baseline horizon (BASELINE_DAYS) so the
|
|
360
|
+
// expectation has a stable sample; every number below is reported to the user under the
|
|
361
|
+
// SELECTED window's heading, so it must fold `windowPrs`, never `base.prs`.
|
|
362
|
+
const windowPrs = base.prs.filter((p) => p.inWindow);
|
|
363
|
+
for (const pr of windowPrs) {
|
|
364
|
+
if (pr.loc != null)
|
|
365
|
+
sizedPrs += 1;
|
|
366
|
+
if (pr.total > 0)
|
|
367
|
+
prsWithBotComments += 1;
|
|
368
|
+
if (pr.total > maxOnOnePr)
|
|
369
|
+
maxOnOnePr = pr.total;
|
|
370
|
+
comments += pr.total;
|
|
371
|
+
for (const [userId, n] of pr.byBot) {
|
|
372
|
+
let a = byBot.get(userId);
|
|
373
|
+
if (!a) {
|
|
374
|
+
a = { comments: 0, prs: new Set(), max: 0 };
|
|
375
|
+
byBot.set(userId, a);
|
|
376
|
+
}
|
|
377
|
+
a.comments += n;
|
|
378
|
+
a.prs.add(pr.id);
|
|
379
|
+
if (n > a.max)
|
|
380
|
+
a.max = n;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
const identities = await loadBotIdentities(accountId, scope, [...byBot.keys()]);
|
|
384
|
+
const prs = windowPrs.length;
|
|
385
|
+
const bots = [...byBot.entries()]
|
|
386
|
+
.map(([userId, a]) => {
|
|
387
|
+
const id = identities.get(userId);
|
|
388
|
+
return {
|
|
389
|
+
key: `u${userId}`,
|
|
390
|
+
authorUserId: userId,
|
|
391
|
+
label: id?.label ?? `#${userId}`,
|
|
392
|
+
login: id?.login ?? null,
|
|
393
|
+
kind: id?.kind ?? 'in_house',
|
|
394
|
+
role: id?.role ?? 'review',
|
|
395
|
+
comments: a.comments,
|
|
396
|
+
prsCommentedOn: a.prs.size,
|
|
397
|
+
// ⚠ TWO DENOMINATORS, NAMED APART ON PURPOSE — see the shared type. On three.js the same
|
|
398
|
+
// bot reads ~6× higher per-commented-PR than per-scope-PR (656 of 796 merged PRs draw
|
|
399
|
+
// nothing), and nothing in the number itself tells them apart.
|
|
400
|
+
avgCommentsPerCommentedPr: avg2(a.comments, a.prs.size),
|
|
401
|
+
avgCommentsPerScopePr: avg2(a.comments, prs),
|
|
402
|
+
maxCommentsOnOnePr: a.max,
|
|
403
|
+
};
|
|
404
|
+
})
|
|
405
|
+
// Most comments first; label as a stable tiebreak so two equal bots keep a fixed order
|
|
406
|
+
// across requests (a table that reshuffles on refresh reads as data changing).
|
|
407
|
+
.sort((x, y) => y.comments - x.comments || x.label.localeCompare(y.label));
|
|
408
|
+
const totals = {
|
|
409
|
+
prs,
|
|
410
|
+
sizedPrs,
|
|
411
|
+
comments,
|
|
412
|
+
prsWithBotComments,
|
|
413
|
+
prsWithNoBotComments: prs - prsWithBotComments,
|
|
414
|
+
avgCommentsPerCommentedPr: avg2(comments, prsWithBotComments),
|
|
415
|
+
avgCommentsPerScopePr: avg2(comments, prs),
|
|
416
|
+
maxCommentsOnOnePr: maxOnOnePr,
|
|
417
|
+
};
|
|
418
|
+
return {
|
|
419
|
+
workspaceId: scope.workspaceId,
|
|
420
|
+
window: win,
|
|
421
|
+
bots,
|
|
422
|
+
totals,
|
|
423
|
+
truncated: base.truncated,
|
|
424
|
+
generatedAt: base.generatedAt,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Build both baselines from a per-PR count function.
|
|
429
|
+
*
|
|
430
|
+
* ⚠ IT TAKES `countOf` RATHER THAN READING `pr.total` because `refine.authorUserIds` MOVES THE
|
|
431
|
+
* BASELINE. Comparing one bot's 12 comments against every bot's combined expectation would make
|
|
432
|
+
* every single-bot ratio read low, and uniformly so — the most convincing kind of wrong number.
|
|
433
|
+
*/
|
|
434
|
+
function buildBaselines(prs, countOf) {
|
|
435
|
+
const byRepoBucket = new Map();
|
|
436
|
+
const byRepo = new Map();
|
|
437
|
+
for (const pr of prs) {
|
|
438
|
+
const n = countOf(pr);
|
|
439
|
+
const repo = byRepo.get(pr.repoId) ?? { n: 0, sum: 0 };
|
|
440
|
+
repo.n += 1;
|
|
441
|
+
repo.sum += n;
|
|
442
|
+
byRepo.set(pr.repoId, repo);
|
|
443
|
+
if (pr.bucket == null)
|
|
444
|
+
continue; // unsized PRs condition nothing
|
|
445
|
+
const key = `${pr.repoId}:${pr.bucket}`;
|
|
446
|
+
const cell = byRepoBucket.get(key) ?? { n: 0, sum: 0 };
|
|
447
|
+
cell.n += 1;
|
|
448
|
+
cell.sum += n;
|
|
449
|
+
byRepoBucket.set(key, cell);
|
|
450
|
+
}
|
|
451
|
+
return { byRepoBucket, byRepo };
|
|
452
|
+
}
|
|
453
|
+
function expectationFor(pr, baselines) {
|
|
454
|
+
// No observed size ⇒ no size-conditioned claim, and the repo mean would be answering a question
|
|
455
|
+
// nobody asked (it is a FALLBACK for a thin bucket, not a size-free expectation).
|
|
456
|
+
if (pr.bucket == null)
|
|
457
|
+
return { expected: null, baseline: 'none', baselinePrs: 0 };
|
|
458
|
+
const cell = baselines.byRepoBucket.get(`${pr.repoId}:${pr.bucket}`);
|
|
459
|
+
if (cell && cell.n >= BASELINE_MIN_PRS) {
|
|
460
|
+
const expected = round2(cell.sum / cell.n);
|
|
461
|
+
// ⚠ SAMPLE SIZE PASSED; THE EXPECTATION ITSELF IS TOO SMALL TO DIVIDE BY. Reported as its own
|
|
462
|
+
// kind rather than folded into 'none' so the row can still SHOW what it was measured against
|
|
463
|
+
// (`expected` + `baselinePrs` stay populated) while withholding the multiplier. The two
|
|
464
|
+
// failures read identically as a null ratio and are not the same fact: 'none' means "we could
|
|
465
|
+
// not find comparable PRs", this means "we found plenty and they are all near zero".
|
|
466
|
+
if (expected < BASELINE_MIN_EXPECTED) {
|
|
467
|
+
return { expected, baseline: 'low_expectation', baselinePrs: cell.n };
|
|
468
|
+
}
|
|
469
|
+
return { expected, baseline: 'bucket', baselinePrs: cell.n };
|
|
470
|
+
}
|
|
471
|
+
const repo = baselines.byRepo.get(pr.repoId);
|
|
472
|
+
if (repo && repo.n >= BASELINE_MIN_PRS) {
|
|
473
|
+
const expected = round2(repo.sum / repo.n);
|
|
474
|
+
if (expected < BASELINE_MIN_EXPECTED) {
|
|
475
|
+
return { expected, baseline: 'low_expectation', baselinePrs: repo.n };
|
|
476
|
+
}
|
|
477
|
+
return { expected, baseline: 'repo', baselinePrs: repo.n };
|
|
478
|
+
}
|
|
479
|
+
return { expected: null, baseline: 'none', baselinePrs: 0 };
|
|
480
|
+
}
|
|
481
|
+
function ratioFor(botComments, expected, baseline) {
|
|
482
|
+
// The expectation floor is enforced HERE, at the one place a multiplier is produced, so no
|
|
483
|
+
// caller can route around it by reading `expected` and dividing for itself.
|
|
484
|
+
if (baseline === 'low_expectation')
|
|
485
|
+
return null;
|
|
486
|
+
// `expected === 0` cannot hide a finding: the PR is a member of its own baseline population, so
|
|
487
|
+
// a mean of 0 forces this PR's own count to 0 too. Reporting null beats reporting Infinity or a
|
|
488
|
+
// fabricated 0 — both of which would sort.
|
|
489
|
+
if (expected == null || expected === 0)
|
|
490
|
+
return null;
|
|
491
|
+
return round2(botComments / expected);
|
|
492
|
+
}
|
|
493
|
+
export async function foldPrBotVolumePopulation(accountId, window, scope, refine, sort) {
|
|
494
|
+
const base = await loadVolumeBase(accountId, window, scope);
|
|
495
|
+
const win = {
|
|
496
|
+
kind: base.windowKind,
|
|
497
|
+
from: base.from.toISOString(),
|
|
498
|
+
to: base.to.toISOString(),
|
|
499
|
+
};
|
|
500
|
+
// ⚠ `[]` MEANS "NO BOTS", NEVER "EVERY BOT" — the `repoIds` rule (`if (ids)`, never
|
|
501
|
+
// `ids.length > 0`), and the BotFlaggingRefine contract restated. Only `null` widens.
|
|
502
|
+
const only = refine.authorUserIds == null ? null : new Set(refine.authorUserIds);
|
|
503
|
+
const countOf = (pr) => {
|
|
504
|
+
if (!only)
|
|
505
|
+
return pr.total;
|
|
506
|
+
let n = 0;
|
|
507
|
+
for (const [userId, c] of pr.byBot)
|
|
508
|
+
if (only.has(userId))
|
|
509
|
+
n += c;
|
|
510
|
+
return n;
|
|
511
|
+
};
|
|
512
|
+
// ⚠ THE BASELINE IS THE ONE FOLD ENTITLED TO THE UNFILTERED SET. It spans BASELINE_DAYS on
|
|
513
|
+
// purpose (see BASELINE_SPAN_MS): an expectation tied to the display window moves under the
|
|
514
|
+
// reader and empties out at 7d. Rebuilt over the SAME narrowed bot set — see buildBaselines.
|
|
515
|
+
const baselines = buildBaselines(base.prs, countOf);
|
|
516
|
+
// …and the LIST is the selected window, nothing wider.
|
|
517
|
+
const windowPrs = base.prs.filter((p) => p.inWindow);
|
|
518
|
+
// Only PRs the refined bot set actually touched are enumerated. `total` stays the whole merged
|
|
519
|
+
// population so the caption can say "140 of 796 merged PRs drew bot comments" — the zero rows
|
|
520
|
+
// are the point of that sentence, not rows to page through (three.js: 656 of them).
|
|
521
|
+
const scored = [];
|
|
522
|
+
for (const pr of windowPrs) {
|
|
523
|
+
const botComments = countOf(pr);
|
|
524
|
+
if (botComments === 0)
|
|
525
|
+
continue;
|
|
526
|
+
const { expected, baseline, baselinePrs } = expectationFor(pr, baselines);
|
|
527
|
+
scored.push({
|
|
528
|
+
pr,
|
|
529
|
+
botComments,
|
|
530
|
+
expected,
|
|
531
|
+
ratio: ratioFor(botComments, expected, baseline),
|
|
532
|
+
baseline,
|
|
533
|
+
baselinePrs,
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
// ⚠ EVERY COMPARATOR IS TOTAL. The offset is a slice over this fold, so two rows that compare
|
|
537
|
+
// equal must still hold a fixed order or a cursor walk silently duplicates and drops rows.
|
|
538
|
+
// `prId` ascending is the final tiebreak in both sorts.
|
|
539
|
+
//
|
|
540
|
+
// Under `'ratio'`, rows with NO baseline sort LAST rather than as 0: an unmeasurable PR is not
|
|
541
|
+
// a below-average one, and burying the null rows is the only reading that keeps the top of the
|
|
542
|
+
// list meaningful.
|
|
543
|
+
scored.sort((a, b) => {
|
|
544
|
+
if (sort === 'ratio') {
|
|
545
|
+
const ar = a.ratio;
|
|
546
|
+
const br = b.ratio;
|
|
547
|
+
if (ar == null && br != null)
|
|
548
|
+
return 1;
|
|
549
|
+
if (br == null && ar != null)
|
|
550
|
+
return -1;
|
|
551
|
+
if (ar != null && br != null && ar !== br)
|
|
552
|
+
return br - ar;
|
|
553
|
+
}
|
|
554
|
+
if (b.botComments !== a.botComments)
|
|
555
|
+
return b.botComments - a.botComments;
|
|
556
|
+
const at = a.pr.mergedAt.getTime();
|
|
557
|
+
const bt = b.pr.mergedAt.getTime();
|
|
558
|
+
if (bt !== at)
|
|
559
|
+
return bt - at;
|
|
560
|
+
return a.pr.id - b.pr.id;
|
|
561
|
+
});
|
|
562
|
+
return {
|
|
563
|
+
win,
|
|
564
|
+
generatedAt: base.generatedAt,
|
|
565
|
+
scored,
|
|
566
|
+
windowPrCount: windowPrs.length,
|
|
567
|
+
truncated: base.truncated,
|
|
568
|
+
repoFullName: base.repoFullName,
|
|
569
|
+
only,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
export async function getPrBotVolume(accountId, window, scope, refine, page) {
|
|
573
|
+
const offset = Math.max(0, Math.trunc(page.offset));
|
|
574
|
+
const limit = Math.max(1, Math.min(Math.trunc(page.limit), VOLUME_PAGE_MAX));
|
|
575
|
+
// The ONE population fold (shared with the synthesis input — §8.3).
|
|
576
|
+
const pop = await foldPrBotVolumePopulation(accountId, window, scope, refine, page.sort);
|
|
577
|
+
const { scored, only } = pop;
|
|
578
|
+
const pageRows = scored.slice(offset, offset + limit);
|
|
579
|
+
const consumed = offset + pageRows.length;
|
|
580
|
+
// Identities only for the bots that actually appear on THIS page — the drill-down spans many
|
|
581
|
+
// PRs, so a per-row lookup is out, but so is resolving every bot in the workspace.
|
|
582
|
+
const pageBotIds = new Set();
|
|
583
|
+
for (const s of pageRows) {
|
|
584
|
+
for (const [userId] of s.pr.byBot) {
|
|
585
|
+
if (!only || only.has(userId))
|
|
586
|
+
pageBotIds.add(userId);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
const identities = await loadBotIdentities(accountId, scope, [...pageBotIds]);
|
|
590
|
+
const items = pageRows.map((s) => {
|
|
591
|
+
const byBot = [...s.pr.byBot.entries()]
|
|
592
|
+
.filter(([userId]) => !only || only.has(userId))
|
|
593
|
+
.map(([userId, comments]) => ({
|
|
594
|
+
key: `u${userId}`,
|
|
595
|
+
authorUserId: userId,
|
|
596
|
+
label: identities.get(userId)?.label ?? `#${userId}`,
|
|
597
|
+
comments,
|
|
598
|
+
}))
|
|
599
|
+
.sort((x, y) => y.comments - x.comments || x.label.localeCompare(y.label));
|
|
600
|
+
const repoFullName = pop.repoFullName.get(s.pr.repoId) ?? `#${s.pr.repoId}`;
|
|
601
|
+
return {
|
|
602
|
+
prId: s.pr.id,
|
|
603
|
+
prNumber: s.pr.number,
|
|
604
|
+
prTitle: s.pr.title,
|
|
605
|
+
prUrl: `https://github.com/${repoFullName}/pull/${s.pr.number}`,
|
|
606
|
+
repoId: s.pr.repoId,
|
|
607
|
+
repoFullName,
|
|
608
|
+
createdAt: s.pr.openedAt.toISOString(),
|
|
609
|
+
mergedAt: s.pr.mergedAt.toISOString(),
|
|
610
|
+
additions: s.pr.additions,
|
|
611
|
+
deletions: s.pr.deletions,
|
|
612
|
+
loc: s.pr.loc,
|
|
613
|
+
// Null in lockstep with `loc`: the two are unknown together (they come from the same
|
|
614
|
+
// unhydrated row), and a lone `changedFiles: 0` beside a null LOC reads as a real zero.
|
|
615
|
+
changedFiles: s.pr.loc == null ? null : s.pr.changedFiles,
|
|
616
|
+
botComments: s.botComments,
|
|
617
|
+
byBot,
|
|
618
|
+
sizeBucket: s.pr.bucket,
|
|
619
|
+
expected: s.expected,
|
|
620
|
+
ratio: s.ratio,
|
|
621
|
+
baseline: s.baseline,
|
|
622
|
+
baselinePrs: s.baselinePrs,
|
|
623
|
+
// The raw density, shipped but never ranked on — it explodes on small PRs by construction
|
|
624
|
+
// (see the file header). `max(loc, 1)` keeps a rename-only PR finite.
|
|
625
|
+
commentsPer100Loc: s.pr.loc == null ? null : round2((s.botComments / Math.max(s.pr.loc, 1)) * 100),
|
|
626
|
+
};
|
|
627
|
+
});
|
|
628
|
+
return {
|
|
629
|
+
workspaceId: scope.workspaceId,
|
|
630
|
+
window: pop.win,
|
|
631
|
+
refine,
|
|
632
|
+
sort: page.sort,
|
|
633
|
+
total: pop.windowPrCount,
|
|
634
|
+
filteredTotal: scored.length,
|
|
635
|
+
items,
|
|
636
|
+
// Opaque on the wire; `o:<n>` is today's encoding of "offset into the sorted fold".
|
|
637
|
+
nextCursor: consumed < scored.length ? `o:${consumed}` : null,
|
|
638
|
+
truncated: pop.truncated,
|
|
639
|
+
generatedAt: pop.generatedAt,
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
// ── (3) The LOC-vs-volume chart series ──────────────────────────────────────────────────────
|
|
643
|
+
export async function getBotVolumeScatter(accountId, window, scope) {
|
|
644
|
+
const base = await loadVolumeBase(accountId, window, scope);
|
|
645
|
+
const win = {
|
|
646
|
+
kind: base.windowKind,
|
|
647
|
+
from: base.from.toISOString(),
|
|
648
|
+
to: base.to.toISOString(),
|
|
649
|
+
};
|
|
650
|
+
// Bucket aggregates cover EVERY sized PR the SCAN returned, independently of the point cap — the
|
|
651
|
+
// expectation curve must describe the whole corpus even when the cloud beneath it is a sample.
|
|
652
|
+
//
|
|
653
|
+
// ⚠ "the scan returned", not "the scope holds": when `base.truncated` is set the scan itself
|
|
654
|
+
// stopped at VOLUME_PR_SCAN_CAP, so these means describe the most-recent 5000 merged PRs and not
|
|
655
|
+
// the window. That is reported as `scanTruncated` — a field DISTINCT from `truncated` (the point
|
|
656
|
+
// cap), because the two weaken different claims: `truncated` means "not every dot is drawn",
|
|
657
|
+
// `scanTruncated` means "the curve those dots are judged against is itself a sample". Folding
|
|
658
|
+
// them into one flag would leave a reader unable to tell which, and a sampled baseline presented
|
|
659
|
+
// as the scope's baseline is precisely the sin the post-merge autopsy was deleted for.
|
|
660
|
+
const agg = new Map();
|
|
661
|
+
const points = [];
|
|
662
|
+
let sizedPrs = 0;
|
|
663
|
+
let unsizedPrs = 0;
|
|
664
|
+
// ⚠ WINDOW ONLY — this card is captioned with the selected window, so its bars must describe
|
|
665
|
+
// that window. The wider scan exists for the drill-down's expectation, not for this chart.
|
|
666
|
+
const windowPrs = base.prs.filter((p) => p.inWindow);
|
|
667
|
+
for (const pr of windowPrs) {
|
|
668
|
+
if (pr.loc == null || pr.bucket == null) {
|
|
669
|
+
unsizedPrs += 1;
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
sizedPrs += 1;
|
|
673
|
+
const a = agg.get(pr.bucket) ?? { prs: 0, comments: 0, loc: 0 };
|
|
674
|
+
a.prs += 1;
|
|
675
|
+
a.comments += pr.total;
|
|
676
|
+
a.loc += Math.max(pr.loc, 1);
|
|
677
|
+
agg.set(pr.bucket, a);
|
|
678
|
+
// `base.prs` is newest-merged first, so the cap keeps the most recent points.
|
|
679
|
+
if (points.length < SCATTER_POINT_CAP) {
|
|
680
|
+
points.push({
|
|
681
|
+
prId: pr.id,
|
|
682
|
+
repoId: pr.repoId,
|
|
683
|
+
loc: pr.loc,
|
|
684
|
+
changedFiles: pr.changedFiles,
|
|
685
|
+
botComments: pr.total,
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
// Dense: every bucket present even at zero, so a gap in the curve is a gap in the DATA and not
|
|
690
|
+
// an omitted key the chart has to guess about.
|
|
691
|
+
const buckets = SIZE_BUCKETS.map((b) => {
|
|
692
|
+
const a = agg.get(b.bucket) ?? { prs: 0, comments: 0, loc: 0 };
|
|
693
|
+
return {
|
|
694
|
+
bucket: b.bucket,
|
|
695
|
+
label: b.label,
|
|
696
|
+
minLoc: b.minLoc,
|
|
697
|
+
maxLoc: b.maxLoc,
|
|
698
|
+
prs: a.prs,
|
|
699
|
+
comments: a.comments,
|
|
700
|
+
avgComments: avg2(a.comments, a.prs),
|
|
701
|
+
// The ONE place a density belongs: aggregated per bucket it is stable and it is exactly the
|
|
702
|
+
// sublinearity readout (measured on erxes: 32.91 → 8.33 → 4.32 → 2.08 → 0.30).
|
|
703
|
+
commentsPer100Loc: a.loc > 0 ? round2((a.comments / a.loc) * 100) : null,
|
|
704
|
+
};
|
|
705
|
+
});
|
|
706
|
+
return {
|
|
707
|
+
workspaceId: scope.workspaceId,
|
|
708
|
+
window: win,
|
|
709
|
+
points,
|
|
710
|
+
buckets,
|
|
711
|
+
sizedPrs,
|
|
712
|
+
unsizedPrs,
|
|
713
|
+
// The POINT cap, not the scan cap — reported separately so "showing the most recent 2000"
|
|
714
|
+
// does not imply the aggregates beside it were sampled too.
|
|
715
|
+
truncated: sizedPrs > points.length,
|
|
716
|
+
// The SCAN cap, straight off the shared loader — the same bit `/volume` and `/volume/prs`
|
|
717
|
+
// surface, so all three routes agree about whether the window was fully walked.
|
|
718
|
+
scanTruncated: base.truncated,
|
|
719
|
+
generatedAt: base.generatedAt,
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
//# sourceMappingURL=bot-volume.js.map
|