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,579 @@
|
|
|
1
|
+
// ── "Same-line overlap" drill-down — the CLUSTER half of the bot-flagging drill-down ────────
|
|
2
|
+
//
|
|
3
|
+
// The Bots rail's ML strip ends with a "Same-line overlap" tile: how many line areas more than
|
|
4
|
+
// one bot flagged. This file is what happens when it is clicked. Everything else the strip
|
|
5
|
+
// drills into is an ML LABEL population (db/ml-labels.ts, `getBotFlaggingComments`); this tile
|
|
6
|
+
// alone is DETERMINISTIC — no model, no severity — so it lives here and answers a different
|
|
7
|
+
// item shape (`BotFlaggingClustersResponse`).
|
|
8
|
+
//
|
|
9
|
+
// ⚠ THE ONE HARD REQUIREMENT: `total` must equal the tile. The tile is
|
|
10
|
+
// `getBotAnalytics().totals.overlapClusters`, computed in queries.ts by scanning the window's
|
|
11
|
+
// automated-reviewer threads, dropping three populations IN A FIXED ORDER, clustering them with
|
|
12
|
+
// the shared ±3-line arbiter and counting the groups with ≥2 DISTINCT bots. Every one of those
|
|
13
|
+
// steps is reproduced below, in that order, from the same tables — the ONLY intended difference
|
|
14
|
+
// is that the scan reads `gte(createdAt, from)` directly instead of walking the wider 84-day
|
|
15
|
+
// trend span and re-filtering in JS (`from` is the same instant either way, so the two select
|
|
16
|
+
// the same rows). Get any one of the exclusions wrong and the tile says 34 while the list
|
|
17
|
+
// says 41; that is the whole failure mode this comment exists to prevent.
|
|
18
|
+
//
|
|
19
|
+
// ⚑ A SEPARATE FILE, not an addition to ml-labels.ts: this needs `reviewerRoleForUser`, which
|
|
20
|
+
// ml-labels.ts does not import, and adding it there would deepen the deliberate
|
|
21
|
+
// queries ⇄ ml-labels module cycle. Nothing imports this file back, so no new cycle.
|
|
22
|
+
import { and, eq, gte, inArray } from 'drizzle-orm';
|
|
23
|
+
import { db, schema } from './client.js';
|
|
24
|
+
import { botWindowMs } from './bot-window.js';
|
|
25
|
+
import { clusterThreadsByLine } from './line-overlap.js';
|
|
26
|
+
import { labelFor as labelForKind } from '../sync/reviewer-classify.js';
|
|
27
|
+
import { automatedReviewerUserIds, classificationKindForUser, classificationLabelMap, reviewerRoleForUser, } from './queries.js';
|
|
28
|
+
const { mlCommentLabels, pullRequests, repos, reviewComments, reviewThreads, users } = schema;
|
|
29
|
+
// Hydration bound. Clustering itself is free (the Bots panel already runs the strictly WIDER
|
|
30
|
+
// 84-day version of this scan on every load), so `total` is exact and uncapped; this caps only
|
|
31
|
+
// how many clusters get comment bodies + labels attached, and sets `truncated` when it bites.
|
|
32
|
+
const OVERLAP_CLUSTER_CAP = 2000;
|
|
33
|
+
// SQLite's bound-parameter ceiling is per statement, so every `inArray` over an unbounded id
|
|
34
|
+
// list is chunked (the db/retention.ts precedent).
|
|
35
|
+
const MAX_IN_PARAMS = 900;
|
|
36
|
+
// ⚠ LOCAL COPIES OF `ML_SEVERITY_ORD` / `ML_SEVERITIES` ON PURPOSE. Both exist as value exports
|
|
37
|
+
// in @pierre-review/shared, but the backend may import that package with `import type` ONLY —
|
|
38
|
+
// it is types-only and is NOT a shipped dependency, and `build-release.mjs` greps the emitted
|
|
39
|
+
// `release/dist` and FAILS the build on a surviving runtime import. ml-labels.ts's own
|
|
40
|
+
// `SEVERITY_KEYS` is the same accommodation. Keep these in step with the shared exports.
|
|
41
|
+
const SEVERITY_ORD = { nit: 0, minor: 1, major: 2, critical: 3 };
|
|
42
|
+
const SEVERITY_AXIS = ['critical', 'major', 'minor', 'nit']; // worst-first
|
|
43
|
+
const VENDOR_AXIS = [...SEVERITY_AXIS, 'none'];
|
|
44
|
+
const SEVERITY_KEYS = ['nit', 'minor', 'major', 'critical'];
|
|
45
|
+
const VENDOR_CONFIDENCE_KEYS = ['high', 'medium', 'low'];
|
|
46
|
+
const ML_CATEGORY_VALUES = new Set([
|
|
47
|
+
'correctness_bug',
|
|
48
|
+
'security',
|
|
49
|
+
'performance',
|
|
50
|
+
'style_readability',
|
|
51
|
+
'maintainability_refactor',
|
|
52
|
+
'testing',
|
|
53
|
+
'documentation',
|
|
54
|
+
'nitpick',
|
|
55
|
+
'praise',
|
|
56
|
+
]);
|
|
57
|
+
// ── Wire coercions, mirroring ml-labels.ts's `toWireLabel` ──────────────────────────────────
|
|
58
|
+
// Restated here because ml-labels.ts keeps its copies module-private. The drizzle `enum` on
|
|
59
|
+
// these columns is a COMPILE-TIME nicety, not a CHECK constraint, so an off-union stored value
|
|
60
|
+
// is representable: an unreadable severity drops the whole LABEL (the comment still renders,
|
|
61
|
+
// unbadged), while an unreadable vendor claim degrades to "no vendor claim" — the row's own
|
|
62
|
+
// severity is the useful one.
|
|
63
|
+
function coerceSeverity(raw) {
|
|
64
|
+
return typeof raw === 'string' && SEVERITY_KEYS.includes(raw)
|
|
65
|
+
? raw
|
|
66
|
+
: null;
|
|
67
|
+
}
|
|
68
|
+
function coerceVendorConfidence(raw) {
|
|
69
|
+
return typeof raw === 'string' && VENDOR_CONFIDENCE_KEYS.includes(raw)
|
|
70
|
+
? raw
|
|
71
|
+
: null;
|
|
72
|
+
}
|
|
73
|
+
function coerceCategories(raw) {
|
|
74
|
+
if (!Array.isArray(raw))
|
|
75
|
+
return [];
|
|
76
|
+
return raw.filter((c) => typeof c === 'string' && ML_CATEGORY_VALUES.has(c));
|
|
77
|
+
}
|
|
78
|
+
function chunk(arr, n) {
|
|
79
|
+
const out = [];
|
|
80
|
+
for (let i = 0; i < arr.length; i += n)
|
|
81
|
+
out.push(arr.slice(i, i + n));
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
/** Run one `inArray` query per parameter-safe batch and concatenate. Empty in ⇒ no query. */
|
|
85
|
+
async function inChunks(ids, run) {
|
|
86
|
+
const out = [];
|
|
87
|
+
for (const batch of chunk(ids, MAX_IN_PARAMS))
|
|
88
|
+
out.push(...(await run(batch)));
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
// ── The ours-vs-vendor confusion matrix ─────────────────────────────────────────────────────
|
|
92
|
+
// ⚠ A DISPLAY OF TWO CLAIMS, NEVER A RECONCILIATION. `vendorSeverity` is materially LESS
|
|
93
|
+
// accurate than ours on the adjudicated gold-300 (0.474 exact vs 0.700), so it is shown beside
|
|
94
|
+
// ours and read by nothing else: it is not in the cluster predicate, not in `total`, and it
|
|
95
|
+
// never moves a badge. Direction is ordinal on BOTH sides — never "confidence", never
|
|
96
|
+
// `severityProb`.
|
|
97
|
+
function emptyMatrix() {
|
|
98
|
+
const cells = [];
|
|
99
|
+
for (const vendor of VENDOR_AXIS)
|
|
100
|
+
for (const ours of SEVERITY_AXIS)
|
|
101
|
+
cells.push({ vendor, ours, count: 0 });
|
|
102
|
+
return { cells, declared: 0, undeclared: 0, agree: 0, overCall: 0, underCall: 0, total: 0 };
|
|
103
|
+
}
|
|
104
|
+
function buildMatrix(labels) {
|
|
105
|
+
const m = emptyMatrix();
|
|
106
|
+
const index = new Map();
|
|
107
|
+
for (const c of m.cells)
|
|
108
|
+
index.set(`${c.vendor}|${c.ours}`, c);
|
|
109
|
+
for (const l of labels) {
|
|
110
|
+
m.total += 1;
|
|
111
|
+
const vendor = l.vendorSeverity ?? 'none';
|
|
112
|
+
const cell = index.get(`${vendor}|${l.severity}`);
|
|
113
|
+
if (cell)
|
|
114
|
+
cell.count += 1;
|
|
115
|
+
if (l.vendorSeverity == null) {
|
|
116
|
+
m.undeclared += 1;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
m.declared += 1;
|
|
120
|
+
const theirs = SEVERITY_ORD[l.vendorSeverity];
|
|
121
|
+
const ours = SEVERITY_ORD[l.severity];
|
|
122
|
+
if (theirs === ours)
|
|
123
|
+
m.agree += 1;
|
|
124
|
+
else if (theirs > ours)
|
|
125
|
+
m.overCall += 1;
|
|
126
|
+
else
|
|
127
|
+
m.underCall += 1;
|
|
128
|
+
}
|
|
129
|
+
return m;
|
|
130
|
+
}
|
|
131
|
+
/** Does one label satisfy `refine`? A target with NO label never matches a refinement. */
|
|
132
|
+
function labelMatchesRefine(label, refine) {
|
|
133
|
+
if (!label)
|
|
134
|
+
return false;
|
|
135
|
+
if (refine.cell) {
|
|
136
|
+
const vendor = label.vendorSeverity ?? 'none';
|
|
137
|
+
if (vendor !== refine.cell.vendor || label.severity !== refine.cell.ours)
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
if (refine.disagree) {
|
|
141
|
+
// An UNDECLARED vendor claim is not a disagreement — it is silence (the `undeclared`
|
|
142
|
+
// column exists to say so).
|
|
143
|
+
if (label.vendorSeverity == null || label.vendorSeverity === label.severity)
|
|
144
|
+
return false;
|
|
145
|
+
const theirs = SEVERITY_ORD[label.vendorSeverity];
|
|
146
|
+
const ours = SEVERITY_ORD[label.severity];
|
|
147
|
+
if (refine.disagree === 'over' && theirs <= ours)
|
|
148
|
+
return false;
|
|
149
|
+
if (refine.disagree === 'under' && theirs >= ours)
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* The "Same-line overlap" tile's drill-down: the line areas more than one bot flagged, in the
|
|
156
|
+
* SAME window and scope the tile was measured at, one card per cluster.
|
|
157
|
+
*
|
|
158
|
+
* `total` ≡ `getBotAnalytics(...).totals.overlapClusters` BY CONSTRUCTION — same thread
|
|
159
|
+
* population, same three exclusions in the same order, same `clusterThreadsByLine` arbiter,
|
|
160
|
+
* same `userIds.size >= 2` gate. Paging is a JS `slice` over that ordered list behind an opaque
|
|
161
|
+
* cursor, so the count can never be a differently-derived number that happens to agree.
|
|
162
|
+
*/
|
|
163
|
+
export async function getBotOverlapClusters(accountId, refine, window,
|
|
164
|
+
// The SAME BotScope the strip was computed at — `workspaceId` decides who counts as a bot,
|
|
165
|
+
// `repoIds` narrows which data is measured. `repoIds: []` = an empty workspace, never "widen".
|
|
166
|
+
scope, page) {
|
|
167
|
+
const nowMs = Date.now();
|
|
168
|
+
const to = new Date(nowMs);
|
|
169
|
+
// The one shared window→duration mapping (db/bot-window.ts) — the tile's own `from`.
|
|
170
|
+
const from = new Date(nowMs - botWindowMs(window));
|
|
171
|
+
const win = { kind: window, from: from.toISOString(), to: to.toISOString() };
|
|
172
|
+
const generatedAt = to.toISOString();
|
|
173
|
+
const offset = Math.max(0, page.offset);
|
|
174
|
+
const limit = Math.max(1, page.limit);
|
|
175
|
+
const empty = () => ({
|
|
176
|
+
kind: 'clusters',
|
|
177
|
+
workspaceId: scope.workspaceId,
|
|
178
|
+
window: win,
|
|
179
|
+
selector: { kind: 'overlap' },
|
|
180
|
+
refine,
|
|
181
|
+
total: 0,
|
|
182
|
+
filteredTotal: 0,
|
|
183
|
+
matrix: emptyMatrix(),
|
|
184
|
+
items: [],
|
|
185
|
+
nextCursor: null,
|
|
186
|
+
truncated: false,
|
|
187
|
+
generatedAt,
|
|
188
|
+
});
|
|
189
|
+
// An empty workspace → nothing to analyze (mirrors getBotAnalytics' two early exits).
|
|
190
|
+
if (scope.repoIds.length === 0)
|
|
191
|
+
return empty();
|
|
192
|
+
// `role: 'all'` — the same set the tile counted from. The quality-check EXCLUSION happens
|
|
193
|
+
// below, at the same point in the pipeline it happens in getBotAnalytics; narrowing here
|
|
194
|
+
// instead would also change `classificationKindForUser`'s reach and is not the same filter.
|
|
195
|
+
const automatedIds = await automatedReviewerUserIds(accountId, scope.workspaceId, 'all');
|
|
196
|
+
if (automatedIds.length === 0)
|
|
197
|
+
return empty();
|
|
198
|
+
const kindMap = await classificationKindForUser(accountId, scope.workspaceId);
|
|
199
|
+
const roleMap = await reviewerRoleForUser(accountId, scope.workspaceId);
|
|
200
|
+
const classLabel = await classificationLabelMap(accountId, scope.workspaceId);
|
|
201
|
+
// ── 1. The thread scan ────────────────────────────────────────────────────────────────────
|
|
202
|
+
// getBotAnalytics' `threadRows`, with `from` in place of `trendFrom` (it re-filters to the
|
|
203
|
+
// window in JS anyway) plus the PR/repo columns the cards need — carried HERE so the page
|
|
204
|
+
// hydration below fetches bodies and nothing else. `line` lives on `review_threads` and
|
|
205
|
+
// nowhere else: `review_comments` has no line columns, so overlap never computes from
|
|
206
|
+
// comments.
|
|
207
|
+
const threadRows = await db
|
|
208
|
+
.select({
|
|
209
|
+
id: reviewThreads.id,
|
|
210
|
+
prId: reviewThreads.prId,
|
|
211
|
+
userId: reviewThreads.originalCommenterId,
|
|
212
|
+
path: reviewThreads.path,
|
|
213
|
+
line: reviewThreads.line,
|
|
214
|
+
state: reviewThreads.derivedState,
|
|
215
|
+
addressedConfidence: reviewThreads.addressedConfidence,
|
|
216
|
+
prNumber: pullRequests.number,
|
|
217
|
+
prTitle: pullRequests.title,
|
|
218
|
+
prAuthorId: pullRequests.authorId,
|
|
219
|
+
repoId: pullRequests.repoId,
|
|
220
|
+
owner: repos.owner,
|
|
221
|
+
name: repos.name,
|
|
222
|
+
})
|
|
223
|
+
.from(reviewThreads)
|
|
224
|
+
.innerJoin(pullRequests, eq(pullRequests.id, reviewThreads.prId))
|
|
225
|
+
.innerJoin(repos, eq(repos.id, pullRequests.repoId))
|
|
226
|
+
.where(and(eq(pullRequests.accountId, accountId), inArray(reviewThreads.originalCommenterId, automatedIds), gte(reviewThreads.createdAt, from), inArray(pullRequests.repoId, scope.repoIds)))
|
|
227
|
+
.execute();
|
|
228
|
+
// ── 2. The three exclusions, IN THE TILE'S ORDER ──────────────────────────────────────────
|
|
229
|
+
// (a) a thread with no original commenter identifies no bot; (b) an id this workspace has not
|
|
230
|
+
// classified as automated is not a bot HERE (identity is per workspace); (c) quality checks
|
|
231
|
+
// are excluded from overlap on both sides — "SonarQube and CodeRabbit both hit line 42" is a
|
|
232
|
+
// rule firing next to a judgement, not two reviewers agreeing.
|
|
233
|
+
const threads = [];
|
|
234
|
+
for (const t of threadRows) {
|
|
235
|
+
if (t.userId == null)
|
|
236
|
+
continue;
|
|
237
|
+
const kind = kindMap.get(t.userId);
|
|
238
|
+
if (!kind)
|
|
239
|
+
continue;
|
|
240
|
+
// ⚠ `!== 'review'`, never `=== 'quality_check'`: the role union now carries dependency /
|
|
241
|
+
// code_agent / release / housekeeping too, and every one of them would pass a "not a quality
|
|
242
|
+
// check" test back into an overlap number that is only meaningful between REVIEWERS.
|
|
243
|
+
// `roleMap` has no entry for an actor with no stored row and no seeded login, and that
|
|
244
|
+
// absence means the historical default 'review' — hence the `?? 'review'`.
|
|
245
|
+
if ((roleMap.get(t.userId) ?? 'review') !== 'review')
|
|
246
|
+
continue;
|
|
247
|
+
threads.push({
|
|
248
|
+
prId: t.prId,
|
|
249
|
+
path: t.path,
|
|
250
|
+
line: t.line,
|
|
251
|
+
userId: t.userId,
|
|
252
|
+
threadId: t.id,
|
|
253
|
+
kind,
|
|
254
|
+
derivedState: t.state,
|
|
255
|
+
addressedConfidence: t.addressedConfidence,
|
|
256
|
+
prNumber: t.prNumber,
|
|
257
|
+
prTitle: t.prTitle,
|
|
258
|
+
prAuthorId: t.prAuthorId ?? null,
|
|
259
|
+
repoId: t.repoId,
|
|
260
|
+
repoFullName: `${t.owner}/${t.name}`,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
// Deterministic input order. The arbiter sorts by line internally and anchors on line VALUES,
|
|
264
|
+
// so pre-sorting cannot move a thread between clusters — but among threads sharing a line the
|
|
265
|
+
// stable sort preserves input order, which out of a join is not a promise. Sorting here is
|
|
266
|
+
// what makes "this bot's FIRST thread in the cluster" (the representative below) the same
|
|
267
|
+
// thread on every run and in both dialects.
|
|
268
|
+
threads.sort((a, b) => a.prId - b.prId ||
|
|
269
|
+
(a.path < b.path ? -1 : a.path > b.path ? 1 : 0) ||
|
|
270
|
+
(a.line ?? Number.MAX_SAFE_INTEGER) - (b.line ?? Number.MAX_SAFE_INTEGER) ||
|
|
271
|
+
a.threadId - b.threadId);
|
|
272
|
+
// ── 3. THE shared ±3-line clustering (db/line-overlap.ts) ─────────────────────────────────
|
|
273
|
+
// `nullLineGroup: false` and `userIds.size >= 2` are the tile's settings, not a choice made
|
|
274
|
+
// here: a thread LOSES its line when it outdates, and a per-file null lump manufactures
|
|
275
|
+
// overlap out of any two chatty bots.
|
|
276
|
+
const qualifying = [];
|
|
277
|
+
for (const c of clusterThreadsByLine(threads, { nullLineGroup: false })) {
|
|
278
|
+
if (c.userIds.size < 2)
|
|
279
|
+
continue;
|
|
280
|
+
// Never null with `nullLineGroup: false`; the guard is what narrows the type honestly.
|
|
281
|
+
if (c.line == null)
|
|
282
|
+
continue;
|
|
283
|
+
let lineEnd = c.line;
|
|
284
|
+
for (const t of c.items)
|
|
285
|
+
if (t.line != null && t.line > lineEnd)
|
|
286
|
+
lineEnd = t.line;
|
|
287
|
+
qualifying.push({
|
|
288
|
+
prId: c.prId,
|
|
289
|
+
path: c.path,
|
|
290
|
+
lineStart: c.line,
|
|
291
|
+
lineEnd,
|
|
292
|
+
bots: c.userIds.size,
|
|
293
|
+
threadCount: c.items.length,
|
|
294
|
+
items: c.items,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
// THE tile's number. Counted before the hydration cap below, so a capped page still reports
|
|
298
|
+
// the honest total (with `truncated` saying the LIST is a sample).
|
|
299
|
+
const total = qualifying.length;
|
|
300
|
+
// ── 4. A TOTAL order ──────────────────────────────────────────────────────────────────────
|
|
301
|
+
// Biggest dedup hit first, like the per-PR rollup — but that one sorts on two keys only and
|
|
302
|
+
// is scoped to a single PR, so it leaves ties in DB order. A PAGED cross-PR list cannot: two
|
|
303
|
+
// requests for the same page must return the same clusters, so the comparator runs all the
|
|
304
|
+
// way down to a unique key.
|
|
305
|
+
qualifying.sort((a, b) => b.bots - a.bots ||
|
|
306
|
+
b.threadCount - a.threadCount ||
|
|
307
|
+
a.prId - b.prId ||
|
|
308
|
+
(a.path < b.path ? -1 : a.path > b.path ? 1 : 0) ||
|
|
309
|
+
a.lineStart - b.lineStart);
|
|
310
|
+
const truncated = qualifying.length > OVERLAP_CLUSTER_CAP;
|
|
311
|
+
const capped = truncated ? qualifying.slice(0, OVERLAP_CLUSTER_CAP) : qualifying;
|
|
312
|
+
if (capped.length === 0)
|
|
313
|
+
return { ...empty(), total, truncated };
|
|
314
|
+
// ── 5. Origin comment per member thread ───────────────────────────────────────────────────
|
|
315
|
+
// The thread's OWN bot's earliest comment, ties broken on the LOWER id. Both halves matter:
|
|
316
|
+
// without the author filter a human's reply can become the "origin", and without the id
|
|
317
|
+
// tiebreak two comments sharing a timestamp resolve differently per dialect. (This is
|
|
318
|
+
// getBotAnalytics' rule, NOT the per-PR dedup's excerpt pick, which has neither.)
|
|
319
|
+
//
|
|
320
|
+
// Fetched for every CAPPED cluster's threads, not just the page's, because the matrix below
|
|
321
|
+
// describes the whole population the page is a window into. No bodies here — ids only.
|
|
322
|
+
const memberThreadIds = [...new Set(capped.flatMap((c) => c.items.map((t) => t.threadId)))];
|
|
323
|
+
const commentRows = await inChunks(memberThreadIds, (batch) => db
|
|
324
|
+
.select({
|
|
325
|
+
id: reviewComments.id,
|
|
326
|
+
threadId: reviewComments.threadId,
|
|
327
|
+
authorId: reviewComments.authorId,
|
|
328
|
+
createdAt: reviewComments.createdAt,
|
|
329
|
+
})
|
|
330
|
+
.from(reviewComments)
|
|
331
|
+
.innerJoin(pullRequests, eq(pullRequests.id, reviewComments.prId))
|
|
332
|
+
// The ids came from an already-scoped scan; the tenancy predicate is still not optional —
|
|
333
|
+
// "the input was trusted" is how an IDOR gets written.
|
|
334
|
+
.where(and(eq(pullRequests.accountId, accountId), inArray(pullRequests.repoId, scope.repoIds), inArray(reviewComments.threadId, batch)))
|
|
335
|
+
.execute());
|
|
336
|
+
const commentsByThread = new Map();
|
|
337
|
+
for (const r of commentRows) {
|
|
338
|
+
const arr = commentsByThread.get(r.threadId) ?? [];
|
|
339
|
+
arr.push({ id: r.id, authorId: r.authorId, at: r.createdAt.getTime() });
|
|
340
|
+
commentsByThread.set(r.threadId, arr);
|
|
341
|
+
}
|
|
342
|
+
const botByThread = new Map();
|
|
343
|
+
for (const c of capped)
|
|
344
|
+
for (const t of c.items)
|
|
345
|
+
botByThread.set(t.threadId, t.userId);
|
|
346
|
+
const originByThread = new Map();
|
|
347
|
+
for (const [threadId, comments] of commentsByThread) {
|
|
348
|
+
const botId = botByThread.get(threadId);
|
|
349
|
+
if (botId == null)
|
|
350
|
+
continue;
|
|
351
|
+
let origin = null;
|
|
352
|
+
for (const c of comments) {
|
|
353
|
+
if (c.authorId !== botId)
|
|
354
|
+
continue;
|
|
355
|
+
if (origin == null || c.at < origin.at || (c.at === origin.at && c.id < origin.id))
|
|
356
|
+
origin = { id: c.id, at: c.at };
|
|
357
|
+
}
|
|
358
|
+
// A thread with no comment by its own author keeps no origin — the member still renders,
|
|
359
|
+
// unbodied (`comment: null`).
|
|
360
|
+
if (origin)
|
|
361
|
+
originByThread.set(threadId, { id: origin.id, createdAt: new Date(origin.at) });
|
|
362
|
+
}
|
|
363
|
+
// ── 6. ML labels for those origin comments ────────────────────────────────────────────────
|
|
364
|
+
// Shipped INLINE on each card (a cross-PR list must never mount the per-PR `['ml-labels',
|
|
365
|
+
// prId]` index per row — that is the ThreadAssessment 60-requests-for-60-empty-boxes
|
|
366
|
+
// failure), and folded into the matrix below.
|
|
367
|
+
const originIds = [...new Set([...originByThread.values()].map((o) => o.id))];
|
|
368
|
+
const labelRows = await inChunks(originIds, (batch) => db
|
|
369
|
+
.select({
|
|
370
|
+
targetId: mlCommentLabels.targetId,
|
|
371
|
+
severity: mlCommentLabels.severity,
|
|
372
|
+
severityOrd: mlCommentLabels.severityOrd,
|
|
373
|
+
severityProb: mlCommentLabels.severityProb,
|
|
374
|
+
vendorSeverity: mlCommentLabels.vendorSeverity,
|
|
375
|
+
vendorSeverityConfidence: mlCommentLabels.vendorSeverityConfidence,
|
|
376
|
+
categories: mlCommentLabels.categories,
|
|
377
|
+
isSummary: mlCommentLabels.isSummary,
|
|
378
|
+
backend: mlCommentLabels.backend,
|
|
379
|
+
modelVersion: mlCommentLabels.modelVersion,
|
|
380
|
+
createdAt: mlCommentLabels.createdAt,
|
|
381
|
+
})
|
|
382
|
+
.from(mlCommentLabels)
|
|
383
|
+
.where(and(eq(mlCommentLabels.accountId, accountId),
|
|
384
|
+
// `targetId` lives in THREE id spaces — the kind is never optional on a lookup.
|
|
385
|
+
eq(mlCommentLabels.targetKind, 'review_comment'), inArray(mlCommentLabels.targetId, batch)))
|
|
386
|
+
.execute());
|
|
387
|
+
const labelByCommentId = new Map();
|
|
388
|
+
for (const r of labelRows) {
|
|
389
|
+
const severity = coerceSeverity(r.severity);
|
|
390
|
+
if (!severity)
|
|
391
|
+
continue;
|
|
392
|
+
labelByCommentId.set(r.targetId, {
|
|
393
|
+
targetKind: 'review_comment',
|
|
394
|
+
targetId: r.targetId,
|
|
395
|
+
severity,
|
|
396
|
+
severityOrd: r.severityOrd,
|
|
397
|
+
severityProb: r.severityProb,
|
|
398
|
+
vendorSeverity: coerceSeverity(r.vendorSeverity),
|
|
399
|
+
vendorSeverityConfidence: coerceVendorConfidence(r.vendorSeverityConfidence),
|
|
400
|
+
categories: coerceCategories(r.categories),
|
|
401
|
+
isSummary: r.isSummary,
|
|
402
|
+
backend: r.backend,
|
|
403
|
+
modelVersion: r.modelVersion,
|
|
404
|
+
createdAt: r.createdAt.toISOString(),
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
const labelForThread = (threadId) => {
|
|
408
|
+
const origin = originByThread.get(threadId);
|
|
409
|
+
return origin ? labelByCommentId.get(origin.id) : undefined;
|
|
410
|
+
};
|
|
411
|
+
// ── 7. The matrix, PRE-refine ─────────────────────────────────────────────────────────────
|
|
412
|
+
// Over every member thread's origin label, so a cell never zeroes itself out once clicked
|
|
413
|
+
// (the commentFacetCounts rule). ⚠ Its `total` is the count of LABELLED origin comments, NOT
|
|
414
|
+
// the cluster `total` above: a cluster is not an ML row, and only a labelled comment can be
|
|
415
|
+
// placed on either axis. `declared`/`undeclared` state the sparsity honestly.
|
|
416
|
+
const matrix = buildMatrix(capped.flatMap((c) => c.items
|
|
417
|
+
.map((t) => labelForThread(t.threadId))
|
|
418
|
+
.filter((l) => l !== undefined)));
|
|
419
|
+
// ── 8. Refine → slice ─────────────────────────────────────────────────────────────────────
|
|
420
|
+
// A cluster survives when ≥1 of its threads matches. (The wire member shape carries no
|
|
421
|
+
// per-member "matched" flag, so a surviving cluster renders whole — the matching member is
|
|
422
|
+
// recognisable from its own badge.)
|
|
423
|
+
//
|
|
424
|
+
// ⚠ `authorUserIds` (the per-bot narrowing) is a fact about the THREAD's author, not about a
|
|
425
|
+
// label, so it is checked on `t.userId` and — on its own — must NOT acquire the "has a label"
|
|
426
|
+
// requirement `labelMatchesRefine` imposes. A cluster whose members are unlabelled is still a
|
|
427
|
+
// cluster those bots are in. Narrowing a CLUSTER list by a bot set keeps every other member on
|
|
428
|
+
// the card on purpose: the card's whole subject is who else flagged the same lines.
|
|
429
|
+
//
|
|
430
|
+
// ⚠ `[]` MEANS "NO BOTS", NOT "EVERY BOT" — the `repoIds` rule. Both the member predicate and
|
|
431
|
+
// the "is there a narrowing at all" gate test PRESENCE (`!= null`), never length, so an empty
|
|
432
|
+
// set answers with an empty page instead of widening to the whole workspace.
|
|
433
|
+
const memberMatches = (t) => {
|
|
434
|
+
if (refine.authorUserIds != null && !refine.authorUserIds.includes(t.userId))
|
|
435
|
+
return false;
|
|
436
|
+
if (!refine.cell && !refine.disagree)
|
|
437
|
+
return true;
|
|
438
|
+
return labelMatchesRefine(labelForThread(t.threadId), refine);
|
|
439
|
+
};
|
|
440
|
+
const narrowed = refine.cell || refine.disagree || refine.authorUserIds != null
|
|
441
|
+
? capped.filter((c) => c.items.some(memberMatches))
|
|
442
|
+
: capped;
|
|
443
|
+
// Describes the LIST being paged, so it counts the capped population: with `truncated` set it
|
|
444
|
+
// is deliberately below `total`, which stays the tile's honest, uncapped number.
|
|
445
|
+
const filteredTotal = narrowed.length;
|
|
446
|
+
const pageClusters = narrowed.slice(offset, offset + limit);
|
|
447
|
+
// `items.length > 0` is load-bearing: an offset PAST the end yields an empty page, and
|
|
448
|
+
// `offset + 0 < filteredTotal` would then hand back a cursor pointing at the start again.
|
|
449
|
+
const nextOffset = offset + pageClusters.length;
|
|
450
|
+
const nextCursor = pageClusters.length > 0 && nextOffset < filteredTotal ? `o:${nextOffset}` : null;
|
|
451
|
+
// ── 9. Collapse members, then hydrate the PAGE only ───────────────────────────────────────
|
|
452
|
+
// One member per BOT (the per-PR dedup's rule): a verbose bot's 23 threads render as one ×23
|
|
453
|
+
// pill, not 23 identical cards. Representative = that bot's first thread in cluster (line)
|
|
454
|
+
// order, which the pre-sort above made deterministic.
|
|
455
|
+
const collapsed = pageClusters.map((c) => {
|
|
456
|
+
const byUser = new Map();
|
|
457
|
+
for (const t of c.items) {
|
|
458
|
+
const arr = byUser.get(t.userId) ?? [];
|
|
459
|
+
arr.push(t);
|
|
460
|
+
byUser.set(t.userId, arr);
|
|
461
|
+
}
|
|
462
|
+
return { cluster: c, byUser };
|
|
463
|
+
});
|
|
464
|
+
// Only the representative's comment is rendered, so only its body is fetched.
|
|
465
|
+
const pageOriginIds = [
|
|
466
|
+
...new Set(collapsed.flatMap(({ byUser }) => [...byUser.values()].flatMap((ts) => {
|
|
467
|
+
const origin = originByThread.get(ts[0].threadId);
|
|
468
|
+
return origin ? [origin.id] : [];
|
|
469
|
+
}))),
|
|
470
|
+
];
|
|
471
|
+
const bodyByCommentId = new Map();
|
|
472
|
+
if (pageOriginIds.length > 0) {
|
|
473
|
+
for (const r of await inChunks(pageOriginIds, (batch) => db
|
|
474
|
+
.select({ id: reviewComments.id, body: reviewComments.body })
|
|
475
|
+
.from(reviewComments)
|
|
476
|
+
.innerJoin(pullRequests, eq(pullRequests.id, reviewComments.prId))
|
|
477
|
+
.where(and(eq(pullRequests.accountId, accountId), inArray(pullRequests.repoId, scope.repoIds), inArray(reviewComments.id, batch)))
|
|
478
|
+
.execute()))
|
|
479
|
+
bodyByCommentId.set(r.id, r.body);
|
|
480
|
+
}
|
|
481
|
+
// Author identity for the page's bots. Label preference mirrors getBotAnalytics'
|
|
482
|
+
// `reviewerLabel` exactly: the workspace's custom label → the vendor's pretty name (known
|
|
483
|
+
// vendors) → login/display name. Safe to read the GLOBAL users row here — the classification
|
|
484
|
+
// above proves the account association.
|
|
485
|
+
const pageUserIds = [...new Set(collapsed.flatMap(({ byUser }) => [...byUser.keys()]))];
|
|
486
|
+
const displayById = new Map();
|
|
487
|
+
const loginById = new Map();
|
|
488
|
+
for (const r of await inChunks(pageUserIds, (batch) => db
|
|
489
|
+
.select({ id: users.id, login: users.githubLogin, name: users.displayName })
|
|
490
|
+
.from(users)
|
|
491
|
+
.where(inArray(users.id, batch))
|
|
492
|
+
.execute())) {
|
|
493
|
+
displayById.set(r.id, r.name?.trim() || r.login || `#${r.id}`);
|
|
494
|
+
if (r.login)
|
|
495
|
+
loginById.set(r.id, r.login);
|
|
496
|
+
}
|
|
497
|
+
const reviewerLabel = (userId, kind) => {
|
|
498
|
+
const custom = classLabel.get(userId);
|
|
499
|
+
if (custom)
|
|
500
|
+
return custom;
|
|
501
|
+
if (kind !== 'in_house' && kind !== 'pierre' && kind !== 'vendor')
|
|
502
|
+
return labelForKind(kind);
|
|
503
|
+
return displayById.get(userId) ?? labelForKind(kind);
|
|
504
|
+
};
|
|
505
|
+
const items = collapsed.map(({ cluster, byUser }) => {
|
|
506
|
+
const head = cluster.items[0];
|
|
507
|
+
const prUrl = `https://github.com/${head.repoFullName}/pull/${head.prNumber}`;
|
|
508
|
+
const members = [...byUser.values()].map((ts) => {
|
|
509
|
+
const rep = ts[0];
|
|
510
|
+
const origin = originByThread.get(rep.threadId);
|
|
511
|
+
const comment = origin
|
|
512
|
+
? {
|
|
513
|
+
targetKind: 'review_comment',
|
|
514
|
+
targetId: origin.id,
|
|
515
|
+
prId: rep.prId,
|
|
516
|
+
prNumber: rep.prNumber,
|
|
517
|
+
prTitle: rep.prTitle,
|
|
518
|
+
prAuthorId: rep.prAuthorId,
|
|
519
|
+
repoId: rep.repoId,
|
|
520
|
+
repoFullName: rep.repoFullName,
|
|
521
|
+
path: rep.path,
|
|
522
|
+
threadId: rep.threadId,
|
|
523
|
+
derivedState: rep.derivedState,
|
|
524
|
+
// A body row that vanished between the two reads leaves the card unbodied rather
|
|
525
|
+
// than dropping the member — the thread's own metadata is still true.
|
|
526
|
+
body: bodyByCommentId.get(origin.id) ?? null,
|
|
527
|
+
createdAt: origin.createdAt.toISOString(),
|
|
528
|
+
mlLabel: labelByCommentId.get(origin.id) ?? null,
|
|
529
|
+
authorUserId: rep.userId,
|
|
530
|
+
authorLogin: loginById.get(rep.userId) ?? null,
|
|
531
|
+
authorLabel: reviewerLabel(rep.userId, rep.kind),
|
|
532
|
+
authorKind: rep.kind,
|
|
533
|
+
line: rep.line,
|
|
534
|
+
prUrl,
|
|
535
|
+
}
|
|
536
|
+
: null;
|
|
537
|
+
return {
|
|
538
|
+
threadId: rep.threadId,
|
|
539
|
+
threadIds: ts.map((t) => t.threadId),
|
|
540
|
+
line: rep.line,
|
|
541
|
+
derivedState: rep.derivedState,
|
|
542
|
+
addressedConfidence: rep.addressedConfidence,
|
|
543
|
+
comment,
|
|
544
|
+
};
|
|
545
|
+
});
|
|
546
|
+
return {
|
|
547
|
+
// Opaque, equality only — and keyed on `prId`, never a bare PR number (numbers are unique
|
|
548
|
+
// per REPO, so a bare number cross-links one repo's #12 onto another's).
|
|
549
|
+
clusterId: `${cluster.prId}:${cluster.lineStart}:${cluster.path}`,
|
|
550
|
+
prId: cluster.prId,
|
|
551
|
+
prNumber: head.prNumber,
|
|
552
|
+
prTitle: head.prTitle,
|
|
553
|
+
prAuthorId: head.prAuthorId,
|
|
554
|
+
repoId: head.repoId,
|
|
555
|
+
repoFullName: head.repoFullName,
|
|
556
|
+
prUrl,
|
|
557
|
+
path: cluster.path,
|
|
558
|
+
lineStart: cluster.lineStart,
|
|
559
|
+
lineEnd: cluster.lineEnd,
|
|
560
|
+
members,
|
|
561
|
+
threadCount: members.reduce((n, m) => n + m.threadIds.length, 0),
|
|
562
|
+
};
|
|
563
|
+
});
|
|
564
|
+
return {
|
|
565
|
+
kind: 'clusters',
|
|
566
|
+
workspaceId: scope.workspaceId,
|
|
567
|
+
window: win,
|
|
568
|
+
selector: { kind: 'overlap' },
|
|
569
|
+
refine,
|
|
570
|
+
total,
|
|
571
|
+
filteredTotal,
|
|
572
|
+
matrix,
|
|
573
|
+
items,
|
|
574
|
+
nextCursor,
|
|
575
|
+
truncated,
|
|
576
|
+
generatedAt,
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
//# sourceMappingURL=bot-overlap.js.map
|