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,213 @@
|
|
|
1
|
+
// WHO DID THE WORK — the seven lanes, resolved per user id for one workspace.
|
|
2
|
+
//
|
|
3
|
+
// The period report's twelve metrics answer "what happened"; this answers "how much of it was a
|
|
4
|
+
// person". Both are needed, because on a real workspace the blend is severe enough to make the
|
|
5
|
+
// headline figures describe nobody:
|
|
6
|
+
//
|
|
7
|
+
// BNG, one fortnight — 117 merged PRs, of which 46 were Dependabot.
|
|
8
|
+
// median PR size: Dependabot 14 lines · humans 142 lines · REPORTED 68
|
|
9
|
+
//
|
|
10
|
+
// That 68 is not a compromise between two numbers, it is a number no pull request in that
|
|
11
|
+
// workspace resembled, and it understated real human PR size by 2.1×.
|
|
12
|
+
//
|
|
13
|
+
// ── WHY LANES AND NOT "BOT vs HUMAN" ─────────────────────────────────────────────────────────
|
|
14
|
+
//
|
|
15
|
+
// Because automation contaminates DIFFERENT metrics depending on what it does, and one bucket
|
|
16
|
+
// cannot tell those apart:
|
|
17
|
+
//
|
|
18
|
+
// • a DEPENDENCY bot authors bumps → distorts throughput, lead time, PR size
|
|
19
|
+
// • a CODE AGENT authors real changes → distorts the same metrics and means the OPPOSITE
|
|
20
|
+
// • a QUALITY GATE responds/approves → distorts review counts and approvals
|
|
21
|
+
// • an AI REVIEWER writes findings → the only automation whose review volume means anything
|
|
22
|
+
// • RELEASE automation merges/tags → its merges are governance events, not work
|
|
23
|
+
// • HOUSEKEEPING greets and labels → pure noise in every one of the above
|
|
24
|
+
//
|
|
25
|
+
// "Your throughput is inflated by bumps" and "your approvals are automated" are different
|
|
26
|
+
// problems with different fixes, and a single `isBot` flag can state neither. The same workspace
|
|
27
|
+
// showed SonarQube posting 786 comments — every one of them a "Quality Gate Passed/Failed" badge
|
|
28
|
+
// — so folding its volume in with an AI reviewer's findings would report 786 pieces of feedback
|
|
29
|
+
// where there were none.
|
|
30
|
+
//
|
|
31
|
+
// The dependency/code-agent split is the one that most resists being collapsed back: both author
|
|
32
|
+
// pull requests, so any "bot vs human" view puts them together, and yet a merged Dependabot bump
|
|
33
|
+
// is overhead a team absorbed while a merged agent PR is work it shipped. A single "automation
|
|
34
|
+
// authored 40% of merges" figure that mixes them tells the reader nothing they can act on.
|
|
35
|
+
//
|
|
36
|
+
// ── THE DUPLICATE-IDENTITY DEFENCE ───────────────────────────────────────────────────────────
|
|
37
|
+
//
|
|
38
|
+
// Real accounts carry the SAME actor as two user rows with CONFLICTING flags: `dependabot` and
|
|
39
|
+
// `dependabot[bot]`, `github-actions` and `github-actions[bot]`. On the measured account one of
|
|
40
|
+
// each pair sat at `workspace_reviewers.automated = 0`, i.e. counted as a human. Merging the rows
|
|
41
|
+
// is not an option — they have different GitHub node ids and may be genuinely different accounts
|
|
42
|
+
// (an App vs a user of the same name) — so this resolver never trusts a single signal:
|
|
43
|
+
//
|
|
44
|
+
// automated ⇐ workspace verdict ∪ users.isBot ∪ the login vocabularies
|
|
45
|
+
// human ⇐ only when NO signal fires, or a human explicitly said so
|
|
46
|
+
//
|
|
47
|
+
// A genuine manual "this is a person" still wins (that rule is load-bearing elsewhere and is not
|
|
48
|
+
// weakened here); what does not win is the mere ABSENCE of an automated verdict.
|
|
49
|
+
import { inArray } from 'drizzle-orm';
|
|
50
|
+
import { db, schema } from './client.js';
|
|
51
|
+
import { roleForBotLogin } from '../sync/bot-detection.js';
|
|
52
|
+
import { automatedReviewerUserIds, classificationKindForUser, manualHumanUserIds, manualRoleUserIds, reviewerRoleForUser, } from './queries.js';
|
|
53
|
+
const { users } = schema;
|
|
54
|
+
/** The vendor `kind`s that denote SUBSTANTIVE AI review, as opposed to a scanner wearing a vendor
|
|
55
|
+
* brand. Anything classified with one of these is in the `ai_review` lane even if its login is
|
|
56
|
+
* unknown to the local vocabulary — a user's manual vendor classification is a real judgement. */
|
|
57
|
+
const AI_REVIEW_KINDS = new Set([
|
|
58
|
+
'coderabbit',
|
|
59
|
+
'greptile',
|
|
60
|
+
'copilot',
|
|
61
|
+
'qodo',
|
|
62
|
+
'sourcery',
|
|
63
|
+
'bito',
|
|
64
|
+
'ellipsis',
|
|
65
|
+
'korbit',
|
|
66
|
+
'baz',
|
|
67
|
+
'graphite',
|
|
68
|
+
'cursor',
|
|
69
|
+
'devin',
|
|
70
|
+
'entelligence',
|
|
71
|
+
'codex',
|
|
72
|
+
// `vendor` = a generic proprietary reviewer a HUMAN classified when it wasn't a known brand.
|
|
73
|
+
// That is a real judgement that the actor reviews, so it belongs here.
|
|
74
|
+
'vendor',
|
|
75
|
+
]);
|
|
76
|
+
// ⚠ `in_house` IS NOT IN THAT SET, and the shared type's description ("the account's OWN AI")
|
|
77
|
+
// is not what the column actually holds. Measured on the live database: of 37 rows carrying
|
|
78
|
+
// `kind: 'in_house'`, 25 were assigned by `source: 'github_type'` — the fallback for "this is a
|
|
79
|
+
// GitHub App and we don't recognise the brand". That bucket contains sonarqubecloud,
|
|
80
|
+
// dependabot[bot], github-actions[bot], gitguardian, socket-security, google-cla and jit-ci.
|
|
81
|
+
// Not one of them is an AI reviewer.
|
|
82
|
+
//
|
|
83
|
+
// Treating it as one produced the exact failure this module exists to prevent: `github-actions`
|
|
84
|
+
// landed in `quality_gate` while `github-actions[bot]` — the SAME actor, second user row, the one
|
|
85
|
+
// carrying the `in_house` classification — landed in `ai_review`. One CI bot, two lanes, both
|
|
86
|
+
// under-counted, and the "is our AI review tooling earning its licence" number quietly inflated
|
|
87
|
+
// by 384 automated approvals.
|
|
88
|
+
//
|
|
89
|
+
// So an unbranded automation falls through to the `quality_gate` default. That is the asymmetric
|
|
90
|
+
// choice stated in `resolveActorLanes`: under-claiming an AI reviewer costs nothing anyone acts
|
|
91
|
+
// on, over-claiming one corrupts the single number a team would use to cancel a subscription.
|
|
92
|
+
// MIRRORED from `REVIEWER_ROLE_LANE` in @pierre-review/shared — the backend cannot import shared
|
|
93
|
+
// at RUNTIME (a value import fails the release build), so the map is spelled twice and
|
|
94
|
+
// `actor-lanes.test.ts` asserts the two are identical. It is deliberately 1:1: a user who picks
|
|
95
|
+
// "Release automation" and finds the actor filed under "Quality gate" has been told their choice
|
|
96
|
+
// did not take.
|
|
97
|
+
const ROLE_LANE = {
|
|
98
|
+
review: 'ai_review',
|
|
99
|
+
quality_check: 'quality_gate',
|
|
100
|
+
dependency: 'dependency',
|
|
101
|
+
code_agent: 'code_agent',
|
|
102
|
+
release: 'release',
|
|
103
|
+
housekeeping: 'housekeeping',
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* Resolve the lane of every actor that could appear in a workspace's metrics.
|
|
107
|
+
*
|
|
108
|
+
* ORDER IS THE CONTRACT, because the categories genuinely overlap — `github-advanced-security` is
|
|
109
|
+
* a scanner with a vendor brand, SonarQube is a quality check that some accounts have classified
|
|
110
|
+
* by hand, and Copilot both reviews and (elsewhere) authors. Earlier rules win:
|
|
111
|
+
*
|
|
112
|
+
* 1. manual "this is a person" → human (nothing overrules it)
|
|
113
|
+
* 2. no automation signal at all → human
|
|
114
|
+
* 3. MANUAL role → that role's lane
|
|
115
|
+
* 4. known login vocabulary → that role's lane
|
|
116
|
+
* 5. stored role other than 'review' → that role's lane (a seed we derived)
|
|
117
|
+
* 6. vendor kind in AI_REVIEW_KINDS → ai_review
|
|
118
|
+
* 7. anything else automated → quality_gate (CI scripts, unknown apps)
|
|
119
|
+
*
|
|
120
|
+
* ⚠ RULES 3 AND 5 ARE THE SAME COLUMN READ TWICE, AND SPLITTING THEM IS THE POINT. `role` defaults
|
|
121
|
+
* to `'review'`, so a stored `'review'` is ambiguous: it means "a person chose Review bot" on a
|
|
122
|
+
* manual row and "we have never heard of this login" on every other. Collapsing the two either
|
|
123
|
+
* ignores the user's choice (mark Copilot a code agent, watch the report keep calling it AI
|
|
124
|
+
* review) or lets a stale default beat a login we positively recognise. `manualRoleUserIds`
|
|
125
|
+
* carries the disambiguation, and it is automated-rows-only so a manual HUMAN cannot be handed a
|
|
126
|
+
* bot lane by whatever role their row happens to carry.
|
|
127
|
+
*
|
|
128
|
+
* Rule 7 is deliberately NOT `ai_review`, and it is also NOT `housekeeping`: an unrecognised
|
|
129
|
+
* automation is far more likely to be a CI script than a reviewer, and the cost of the mistakes is
|
|
130
|
+
* asymmetric. Miscounting a script as a quality gate understates nothing anyone acts on;
|
|
131
|
+
* miscounting it as an AI reviewer inflates the one number a team would use to judge whether their
|
|
132
|
+
* review tooling is earning its licence, and filing it under housekeeping would quietly drop it
|
|
133
|
+
* out of every count instead of merely declining to credit it.
|
|
134
|
+
*/
|
|
135
|
+
export async function resolveActorLanes(accountId, scope) {
|
|
136
|
+
const [automatedFromWorkspace, kindMap, roleMap, vouchedHuman, manualRoles] = await Promise.all([
|
|
137
|
+
automatedReviewerUserIds(accountId, scope.workspaceId, 'all'),
|
|
138
|
+
classificationKindForUser(accountId, scope.workspaceId),
|
|
139
|
+
reviewerRoleForUser(accountId, scope.workspaceId),
|
|
140
|
+
manualHumanUserIds(accountId, scope.workspaceId),
|
|
141
|
+
manualRoleUserIds(accountId, scope.workspaceId),
|
|
142
|
+
]);
|
|
143
|
+
// ⚠ THE ONE SIGNAL THAT BEATS THE UNION. Widening automation detection to `users.isBot` and the
|
|
144
|
+
// login vocabularies is what fixes the duplicated identities — and it also re-admits an actor a
|
|
145
|
+
// person has explicitly marked human, because such an actor usually has a bot-ish login or a
|
|
146
|
+
// stale global flag, which is exactly WHY someone corrected it by hand.
|
|
147
|
+
const vouched = new Set(vouchedHuman);
|
|
148
|
+
// The GLOBAL bot flag, which catches exactly what the workspace verdict misses: an actor nobody
|
|
149
|
+
// has opened the Bots tab for, and the second row of a duplicated identity. `users` is a global
|
|
150
|
+
// table, so this is read by id — never handed to a tenant.
|
|
151
|
+
const flaggedBotIds = new Set();
|
|
152
|
+
const loginById = new Map();
|
|
153
|
+
const candidateIds = new Set([...automatedFromWorkspace, ...kindMap.keys(), ...roleMap.keys()]);
|
|
154
|
+
const botRows = await db
|
|
155
|
+
.select({ id: users.id, login: users.githubLogin, isBot: users.isBot })
|
|
156
|
+
.from(users)
|
|
157
|
+
.where(inArray(users.isBot, [true]))
|
|
158
|
+
.execute();
|
|
159
|
+
for (const r of botRows) {
|
|
160
|
+
flaggedBotIds.add(r.id);
|
|
161
|
+
loginById.set(r.id, r.login);
|
|
162
|
+
candidateIds.add(r.id);
|
|
163
|
+
}
|
|
164
|
+
// Logins for the workspace-classified ids the global flag did not cover.
|
|
165
|
+
const missing = [...candidateIds].filter((id) => !loginById.has(id));
|
|
166
|
+
if (missing.length > 0) {
|
|
167
|
+
const rows = await db
|
|
168
|
+
.select({ id: users.id, login: users.githubLogin })
|
|
169
|
+
.from(users)
|
|
170
|
+
.where(inArray(users.id, missing))
|
|
171
|
+
.execute();
|
|
172
|
+
for (const r of rows)
|
|
173
|
+
loginById.set(r.id, r.login);
|
|
174
|
+
}
|
|
175
|
+
const workspaceAutomated = new Set(automatedFromWorkspace);
|
|
176
|
+
const lane = new Map();
|
|
177
|
+
const automatedIds = new Set();
|
|
178
|
+
for (const id of candidateIds) {
|
|
179
|
+
if (vouched.has(id))
|
|
180
|
+
continue; // a person said this is a person — nothing overrules that
|
|
181
|
+
const login = loginById.get(id) ?? '';
|
|
182
|
+
// ONE call, not five predicates in an order this file gets to choose: the vocabularies are
|
|
183
|
+
// asserted pairwise disjoint, so `roleForBotLogin` has exactly one answer per login.
|
|
184
|
+
const loginRole = roleForBotLogin(login);
|
|
185
|
+
// THE UNION (see the header). A login vocabulary hit counts on its own, which is what covers
|
|
186
|
+
// the duplicated-identity row whose stored verdict says `automated: 0`.
|
|
187
|
+
const automated = workspaceAutomated.has(id) || flaggedBotIds.has(id) || loginRole != null;
|
|
188
|
+
if (!automated)
|
|
189
|
+
continue;
|
|
190
|
+
automatedIds.add(id);
|
|
191
|
+
const manualRole = manualRoles.get(id);
|
|
192
|
+
const storedRole = roleMap.get(id);
|
|
193
|
+
const kind = kindMap.get(id);
|
|
194
|
+
if (manualRole != null)
|
|
195
|
+
lane.set(id, ROLE_LANE[manualRole]);
|
|
196
|
+
else if (loginRole != null)
|
|
197
|
+
lane.set(id, ROLE_LANE[loginRole]);
|
|
198
|
+
// A DERIVED `'review'` is the default an unknown login gets, so it is not evidence and does
|
|
199
|
+
// not short-circuit; every other derived role was positively concluded and does.
|
|
200
|
+
else if (storedRole != null && storedRole !== 'review')
|
|
201
|
+
lane.set(id, ROLE_LANE[storedRole]);
|
|
202
|
+
else if (kind != null && AI_REVIEW_KINDS.has(kind))
|
|
203
|
+
lane.set(id, 'ai_review');
|
|
204
|
+
else
|
|
205
|
+
lane.set(id, 'quality_gate');
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
lane,
|
|
209
|
+
automatedIds,
|
|
210
|
+
laneOf: (userId) => (userId == null ? 'human' : (lane.get(userId) ?? 'human')),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
//# sourceMappingURL=actor-lanes.js.map
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// ── THE GLOBAL AUTOMATION READERS (CORE) ─────────────────────────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// Two account-free signals that an actor is automation, beside `users.isBot` and the review-bot
|
|
4
|
+
// login table (queries.ts `botUserIds` / `reviewBotUserIds`):
|
|
5
|
+
//
|
|
6
|
+
// • GitHub TYPES the account a Bot (`users.github_type = 'Bot'`) — the App accounts `isLikelyBot`
|
|
7
|
+
// misses because GraphQL returns their login WITHOUT `[bot]`. Measured on the dev DB: seven such
|
|
8
|
+
// users with `is_bot = 0` (Copilot, lumberbot-app, gitguardian, socket-security, google-cla,
|
|
9
|
+
// cdp-github-action, jit-ci), who wrote 112 PR comments on open PRs.
|
|
10
|
+
// • the login is a known NON-review vendor (`AUTOMATION_VENDORS`, bare or `[bot]`, plus the
|
|
11
|
+
// per-org prefixes such as Semgrep's `semgrep-code-<org>`) — Renovate, Snyk, ImgBot…
|
|
12
|
+
//
|
|
13
|
+
// A separate module, not private functions in queries.ts, because My Turn's mention scanner
|
|
14
|
+
// (`db/pr-mentions.ts`, which imports nothing but `client.js`) builds its automation set from these
|
|
15
|
+
// too; reaching them through queries.ts would drag that whole module (and its cycle with
|
|
16
|
+
// ml-labels.ts) into it.
|
|
17
|
+
//
|
|
18
|
+
// ⚠ IDS, used only as MEMBERSHIP tests. `users` is a GLOBAL table: nothing here may hand a tenant
|
|
19
|
+
// a row, and a caller that needs a workspace's answer folds these through
|
|
20
|
+
// `resolveWorkspaceReviewers`, where a person's manual "this is a human" still wins.
|
|
21
|
+
// ⚠ ONE reader returns a LOGIN, and it matches EXACT vendor logins only — a public identity, like
|
|
22
|
+
// the REVIEW_BOTS seed. The PREFIX half (`semgrep-code-<org>`) matches logins that name ANOTHER
|
|
23
|
+
// tenant's organisation, so it only ever yields ids. A new reader here returns ids.
|
|
24
|
+
import { eq, inArray, like, or, sql } from 'drizzle-orm';
|
|
25
|
+
import { db, schema } from './client.js';
|
|
26
|
+
import { automationVendorLoginsForSql, automationVendorPrefixes } from '../sync/bot-detection.js';
|
|
27
|
+
const { users } = schema;
|
|
28
|
+
/** Users GitHub itself types as a Bot (`users.github_type = 'Bot'`) — the App accounts
|
|
29
|
+
* `isLikelyBot` misses because GraphQL returns their login WITHOUT `[bot]` (socket-security,
|
|
30
|
+
* gitguardian, jit-ci, Copilot, google-cla, lumberbot-app, cdp-github-action on the dev DB). */
|
|
31
|
+
export async function githubTypeBotUserIds() {
|
|
32
|
+
const rows = await db
|
|
33
|
+
.select({ id: users.id })
|
|
34
|
+
.from(users)
|
|
35
|
+
.where(eq(users.githubType, 'Bot'))
|
|
36
|
+
.execute();
|
|
37
|
+
return rows.map((r) => r.id);
|
|
38
|
+
}
|
|
39
|
+
/** `lower(github_login)` — built per call, never at import: tests that mock `client.js` with a
|
|
40
|
+
* partial schema import this module too. */
|
|
41
|
+
const loweredLogin = () => sql `lower(${users.githubLogin})`;
|
|
42
|
+
/** Users whose login is in AUTOMATION_VENDORS (bare or `[bot]`) or matches a vendor PREFIX:
|
|
43
|
+
* `lower(github_login) IN (…) OR lower(github_login) LIKE 'semgrep-code-%' OR … 'semgrepcode-%'`.
|
|
44
|
+
* ⚠ Lowercased on BOTH sides, so pg's case-sensitive `LIKE` and SQLite's case-blind one agree;
|
|
45
|
+
* no prefix contains `_`, LIKE's single-character wildcard. */
|
|
46
|
+
export async function automationVendorUserIds() {
|
|
47
|
+
const rows = await db
|
|
48
|
+
.select({ id: users.id })
|
|
49
|
+
.from(users)
|
|
50
|
+
.where(or(inArray(loweredLogin(), automationVendorLoginsForSql()), ...automationVendorPrefixes().map((p) => like(loweredLogin(), `${p}%`))))
|
|
51
|
+
.execute();
|
|
52
|
+
return rows.map((r) => r.id);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* THE BALL RULE'S AUTOMATION SET — every actor we know to be automation WITHOUT a workspace:
|
|
56
|
+
* `users.isBot` ∪ `users.github_type = 'Bot'` ∪ the AUTOMATION_VENDORS logins and prefixes. GLOBAL,
|
|
57
|
+
* so the unscoped `getMyTurn` (browser notifications, the CLI) and the scoped one (the board, the
|
|
58
|
+
* brief) agree, and the mention scanner (db/pr-mentions.ts) honours exactly the mentions the ball
|
|
59
|
+
* rule would.
|
|
60
|
+
*
|
|
61
|
+
* WHY NOT `users.isBot` ALONE: My Turn's newer rules read OTHER people's comments ("a person
|
|
62
|
+
* commented after you", "a person replied to you", "a person @-mentioned you"), and on the dev DB
|
|
63
|
+
* the accounts GitHub types as a Bot but `isBot` misses wrote 112 PR comments on 82 open PRs
|
|
64
|
+
* (google-cla 73, socket-security 24, cdp-github-action 12, gitguardian 2, jit-ci 1). Under the
|
|
65
|
+
* `isBot`-only set each of those summons the viewer as though a colleague had answered them. The
|
|
66
|
+
* Pending cards already call the same accounts automation.
|
|
67
|
+
*
|
|
68
|
+
* ⚠ STILL NOT `hiddenBotUserIds`: that union needs a workspaceId, and `getMyTurn` also runs
|
|
69
|
+
* unscoped. A workspace's manual "this is a human" therefore cannot reach this set — accepted,
|
|
70
|
+
* because every member here is an account GitHub types as an App or a known vendor login.
|
|
71
|
+
* ⚠ `users.isBot` keeps its own meaning; this set is a union READ, never a write.
|
|
72
|
+
*/
|
|
73
|
+
export async function globalAutomationUserIds() {
|
|
74
|
+
const [flagged, typed, vendors] = await Promise.all([
|
|
75
|
+
db
|
|
76
|
+
.select({ id: users.id })
|
|
77
|
+
.from(users)
|
|
78
|
+
.where(eq(users.isBot, true))
|
|
79
|
+
.execute()
|
|
80
|
+
.then((rows) => rows.map((r) => r.id)),
|
|
81
|
+
githubTypeBotUserIds(),
|
|
82
|
+
automationVendorUserIds(),
|
|
83
|
+
]);
|
|
84
|
+
return new Set([...flagged, ...typed, ...vendors]);
|
|
85
|
+
}
|
|
86
|
+
/** The users whose login is EXACTLY a vendor login (bare or `[bot]`), with that login — the kind
|
|
87
|
+
* seed in queries.ts (`classificationKindForUser`) names each one's vendor from it. ⚠ NEVER the
|
|
88
|
+
* prefix half: that kind map is also the bot drill-downs' "is this id classified here" gate, which
|
|
89
|
+
* then resolves the login, so a prefix match would read another org's name back to any tenant. */
|
|
90
|
+
export async function exactAutomationVendorUsers() {
|
|
91
|
+
return db
|
|
92
|
+
.select({ id: users.id, login: users.githubLogin })
|
|
93
|
+
.from(users)
|
|
94
|
+
.where(inArray(loweredLogin(), automationVendorLoginsForSql()))
|
|
95
|
+
.execute();
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=automation-ids.js.map
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
// ── THE AUTHORING-AUTOMATION OUTPUT VECTOR (CORE, deterministic) ─────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// What a bot that WRITES PRs produced in one period, in one workspace. The sibling of
|
|
4
|
+
// db/person-period.ts, and it copies that file's discipline wholesale (window-pure two-sided
|
|
5
|
+
// predicates, computed on read, no table, no migration) — but it is deliberately NOT the same
|
|
6
|
+
// vector, because the question is different. A person's numbers answer "how is this teammate
|
|
7
|
+
// doing"; an automation's answer "what is this thing costing us", which is why
|
|
8
|
+
// `prs_merged_without_human_review` and `human_review_comments_received` are here and response
|
|
9
|
+
// times are not.
|
|
10
|
+
//
|
|
11
|
+
// WHY IT EXISTS: the People report's bot sections read `getBotAnalytics`, whose columns are all
|
|
12
|
+
// REVIEW output (threads, comments, acted-on). That is the whole story for the `review` and
|
|
13
|
+
// `quality_check` roles only. For `dependency` / `code_agent` / `release` / `housekeeping`
|
|
14
|
+
// automation every one of those columns is legitimately zero, and a wall of zeros reads as "it
|
|
15
|
+
// did nothing" when the truth is "it did a different thing". The section used to say so in prose
|
|
16
|
+
// and chart nothing; this fold is what it charts instead.
|
|
17
|
+
//
|
|
18
|
+
// ⚠ EVERY WINDOWED METRIC IS WINDOW-PURE with a TWO-SIDED `>= from AND < to` predicate, so a
|
|
19
|
+
// stored/forwarded period stays reproducible. There is deliberately no live-basis escape hatch
|
|
20
|
+
// here (PersonPeriod has three, marked on the wire): every question this vector asks is about
|
|
21
|
+
// what HAPPENED in the window, so a "currently open" count would be the only unreproducible
|
|
22
|
+
// figure on the page and it would not earn its place.
|
|
23
|
+
//
|
|
24
|
+
// ⚠ `prs_merged_without_human_review` looks like it breaks window purity — it asks "did any human
|
|
25
|
+
// ever review this PR?", which is not itself a windowed question. It does not: the population is
|
|
26
|
+
// PRs MERGED INSIDE the window, and a review of a merged PR necessarily predates its merge, so
|
|
27
|
+
// the answer is fixed the moment the PR lands and can never move afterwards. Restricting the
|
|
28
|
+
// review probe to the window would be the bug — a PR opened and reviewed in March and merged in
|
|
29
|
+
// April would count as "merged without review".
|
|
30
|
+
//
|
|
31
|
+
// ⚠ `users` IS A GLOBAL TABLE (the listUsers precedent). The subject is admitted only after an
|
|
32
|
+
// authored-PR probe INSIDE the workspace scope, and only the login and display name ever leave.
|
|
33
|
+
//
|
|
34
|
+
// ⚠ MIRROR IMAGE OF THE PERSON FOLD'S LANE CHECK: person-period refuses anything the lane
|
|
35
|
+
// resolver calls automation; this refuses anything it calls `'human'`. Neither is a login
|
|
36
|
+
// heuristic — both go through db/actor-lanes.ts, so a manual "this account is a person"
|
|
37
|
+
// judgement moves an actor between the two folds in one place.
|
|
38
|
+
import { and, count, desc, eq, gte, inArray, isNull, lt, ne, or } from 'drizzle-orm';
|
|
39
|
+
import { db, schema } from './client.js';
|
|
40
|
+
import { resolveActorLanes } from './actor-lanes.js';
|
|
41
|
+
import { median, round2 } from './period-metrics.js';
|
|
42
|
+
import { PERSON_EVIDENCE_CAP } from './person-period.js';
|
|
43
|
+
import { reviewerRoleForUser } from './queries.js';
|
|
44
|
+
const { pullRequests, repos, reviewComments, reviews, users } = schema;
|
|
45
|
+
// MIRRORED from @pierre-review/shared (AUTOMATION_METRIC_KEYS) for the same types-only reason the
|
|
46
|
+
// person keys are inlined: `shared` ships no runtime. `automation-output.test.ts` imports the
|
|
47
|
+
// shared original and asserts the two spellings are identical, so drift fails CI, not a reader.
|
|
48
|
+
export const AUTOMATION_METRIC_KEYS = [
|
|
49
|
+
'prs_opened',
|
|
50
|
+
'prs_merged',
|
|
51
|
+
'prs_closed_unmerged',
|
|
52
|
+
'merge_rate_pct',
|
|
53
|
+
'median_hours_to_merge',
|
|
54
|
+
'median_pr_size_lines',
|
|
55
|
+
'prs_merged_without_human_review',
|
|
56
|
+
'human_review_comments_received',
|
|
57
|
+
'repos_touched',
|
|
58
|
+
];
|
|
59
|
+
/** Repos listed under the vector. Rendering cap only — the `repos_touched` METRIC counts them
|
|
60
|
+
* all, so a workspace with 40 repos reports 40 and lists the busiest few. */
|
|
61
|
+
const REPO_LIST_CAP = 8;
|
|
62
|
+
/** The merged-PR scan that feeds the medians and the no-human-review probe. Bounded for the same
|
|
63
|
+
* reason PERIOD_FIRST_REVIEW_PR_CAP is: the ids travel onward as BIND PARAMETERS. A dependency
|
|
64
|
+
* bot can merge hundreds of PRs in a fortnight, so this is the one fold here that can get big. */
|
|
65
|
+
const AUTOMATION_MERGED_PR_CAP = 2000;
|
|
66
|
+
function cell(key, value, sampleSize) {
|
|
67
|
+
return { key, value, sampleSize };
|
|
68
|
+
}
|
|
69
|
+
/** A count is never null — zero merges IS zero, not "no sample". Medians and rates are the ones
|
|
70
|
+
* that go null (the period-metrics rule: a null renders "—", never 0). */
|
|
71
|
+
function countCell(key, n) {
|
|
72
|
+
return cell(key, n, n);
|
|
73
|
+
}
|
|
74
|
+
export async function getAutomationOutput(accountId, scope, userId, window, opts = {}) {
|
|
75
|
+
const from = new Date(window.fromMs);
|
|
76
|
+
const to = new Date(window.toMs);
|
|
77
|
+
// An empty workspace admits nobody — and without this guard the probe below would read the
|
|
78
|
+
// whole account (`inArray(…, [])` is false, but be explicit about the state).
|
|
79
|
+
if (scope.repoIds.length === 0)
|
|
80
|
+
return null;
|
|
81
|
+
const lanes = await resolveActorLanes(accountId, scope);
|
|
82
|
+
if (lanes.laneOf(userId) === 'human')
|
|
83
|
+
return null;
|
|
84
|
+
const inScope = and(eq(pullRequests.accountId, accountId), inArray(pullRequests.repoId, scope.repoIds));
|
|
85
|
+
const authored = and(inScope, eq(pullRequests.authorId, userId));
|
|
86
|
+
// ── Admission probe (ALL TIME, inside the scope) ────────────────────────────────────────────
|
|
87
|
+
// An automation that has never authored a PR in this workspace is not this fold's subject —
|
|
88
|
+
// it is a reviewer, and `getBotAnalytics` already describes it. Returning null here is what
|
|
89
|
+
// lets the caller decide which of the two sections to render, and it is also why the route is
|
|
90
|
+
// not an existence oracle: an unknown id degrades identically.
|
|
91
|
+
const [probe] = await db
|
|
92
|
+
.select({ c: count() })
|
|
93
|
+
.from(pullRequests)
|
|
94
|
+
.where(authored)
|
|
95
|
+
.execute();
|
|
96
|
+
if ((probe?.c ?? 0) === 0)
|
|
97
|
+
return null;
|
|
98
|
+
const [identity] = await db
|
|
99
|
+
.select({ login: users.githubLogin, name: users.displayName })
|
|
100
|
+
.from(users)
|
|
101
|
+
.where(eq(users.id, userId))
|
|
102
|
+
.execute();
|
|
103
|
+
// ── The windowed counts ────────────────────────────────────────────────────────────────────
|
|
104
|
+
const [openedRow, mergedRow, closedRow, humanCommentsRow, repoRows, mergedPrs] = await Promise.all([
|
|
105
|
+
db
|
|
106
|
+
.select({ c: count() })
|
|
107
|
+
.from(pullRequests)
|
|
108
|
+
.where(and(authored, gte(pullRequests.openedAt, from), lt(pullRequests.openedAt, to)))
|
|
109
|
+
.execute(),
|
|
110
|
+
db
|
|
111
|
+
.select({ c: count() })
|
|
112
|
+
.from(pullRequests)
|
|
113
|
+
.where(and(authored, gte(pullRequests.mergedAt, from), lt(pullRequests.mergedAt, to)))
|
|
114
|
+
.execute(),
|
|
115
|
+
// Closed WITHOUT merging, inside the window. `mergedAt IS NULL` is the discriminator —
|
|
116
|
+
// GitHub stamps closedAt on a merge too, so a bare closedAt window would double-count every
|
|
117
|
+
// merged PR as wasted churn.
|
|
118
|
+
db
|
|
119
|
+
.select({ c: count() })
|
|
120
|
+
.from(pullRequests)
|
|
121
|
+
.where(and(authored, isNull(pullRequests.mergedAt), gte(pullRequests.closedAt, from), lt(pullRequests.closedAt, to)))
|
|
122
|
+
.execute(),
|
|
123
|
+
// Human review comments left ON its PRs in the window — the time it cost people. Bot-authored
|
|
124
|
+
// comments are excluded through the lane resolver, not a login test, so one bot reviewing
|
|
125
|
+
// another bot's PRs never reads as human attention.
|
|
126
|
+
db
|
|
127
|
+
.select({ authorId: reviewComments.authorId, c: count() })
|
|
128
|
+
.from(reviewComments)
|
|
129
|
+
.innerJoin(pullRequests, eq(pullRequests.id, reviewComments.prId))
|
|
130
|
+
.where(and(authored, gte(reviewComments.createdAt, from), lt(reviewComments.createdAt, to)))
|
|
131
|
+
.groupBy(reviewComments.authorId)
|
|
132
|
+
.execute(),
|
|
133
|
+
// Repos touched: PRs it OPENED or MERGED in the window, by repo. One two-sided predicate per
|
|
134
|
+
// column, OR-ed — a PR opened before the window and merged inside it still counts the repo.
|
|
135
|
+
db
|
|
136
|
+
.select({ repoId: pullRequests.repoId, owner: repos.owner, name: repos.name, c: count() })
|
|
137
|
+
.from(pullRequests)
|
|
138
|
+
.innerJoin(repos, eq(repos.id, pullRequests.repoId))
|
|
139
|
+
.where(and(authored,
|
|
140
|
+
// Portable `or(and(…), and(…))`, never a raw `sql` template: drizzle binds a template's
|
|
141
|
+
// `${date}` straight through, and better-sqlite3 refuses a Date — the same expression
|
|
142
|
+
// that type-checks fine throws at the first call. Two-sided on both columns.
|
|
143
|
+
or(and(gte(pullRequests.openedAt, from), lt(pullRequests.openedAt, to)), and(gte(pullRequests.mergedAt, from), lt(pullRequests.mergedAt, to)))))
|
|
144
|
+
.groupBy(pullRequests.repoId, repos.owner, repos.name)
|
|
145
|
+
.orderBy(desc(count()))
|
|
146
|
+
.execute(),
|
|
147
|
+
// The merged-in-window population, carrying everything the medians need in one read.
|
|
148
|
+
db
|
|
149
|
+
.select({
|
|
150
|
+
id: pullRequests.id,
|
|
151
|
+
openedAt: pullRequests.openedAt,
|
|
152
|
+
mergedAt: pullRequests.mergedAt,
|
|
153
|
+
additions: pullRequests.additions,
|
|
154
|
+
deletions: pullRequests.deletions,
|
|
155
|
+
})
|
|
156
|
+
.from(pullRequests)
|
|
157
|
+
.where(and(authored, gte(pullRequests.mergedAt, from), lt(pullRequests.mergedAt, to)))
|
|
158
|
+
.orderBy(desc(pullRequests.mergedAt))
|
|
159
|
+
.limit(AUTOMATION_MERGED_PR_CAP)
|
|
160
|
+
.execute(),
|
|
161
|
+
]);
|
|
162
|
+
const opened = openedRow[0]?.c ?? 0;
|
|
163
|
+
const merged = mergedRow[0]?.c ?? 0;
|
|
164
|
+
const closedUnmerged = closedRow[0]?.c ?? 0;
|
|
165
|
+
const humanComments = humanCommentsRow
|
|
166
|
+
.filter((r) => lanes.laneOf(r.authorId) === 'human')
|
|
167
|
+
.reduce((sum, r) => sum + r.c, 0);
|
|
168
|
+
// Merge rate over the RESOLVED population (merged + closed-unmerged, both windowed). Not over
|
|
169
|
+
// `opened`: a PR opened on the last day of the window has not had its chance yet, and dividing
|
|
170
|
+
// by it would make a busy final week look like a failing bot.
|
|
171
|
+
const resolved = merged + closedUnmerged;
|
|
172
|
+
const mergeRate = resolved > 0 ? round2((merged / resolved) * 100) : null;
|
|
173
|
+
const hoursToMerge = mergedPrs
|
|
174
|
+
.map((p) => p.mergedAt != null && p.openedAt != null
|
|
175
|
+
? (p.mergedAt.getTime() - p.openedAt.getTime()) / 3_600_000
|
|
176
|
+
: null)
|
|
177
|
+
.filter((v) => v != null && Number.isFinite(v) && v >= 0);
|
|
178
|
+
const sizes = mergedPrs.map((p) => p.additions + p.deletions);
|
|
179
|
+
// ── "Merged with no human in the loop" ─────────────────────────────────────────────────────
|
|
180
|
+
// Over the merged-in-window ids, ALL-TIME (see the header note on why that is still
|
|
181
|
+
// window-pure). A PR counts as human-touched if any human submitted a review OR left a review
|
|
182
|
+
// comment on it, ever.
|
|
183
|
+
const mergedIds = mergedPrs.map((p) => p.id);
|
|
184
|
+
let mergedNoHuman = null;
|
|
185
|
+
if (mergedIds.length > 0) {
|
|
186
|
+
const [reviewRows, commentRows] = await Promise.all([
|
|
187
|
+
db
|
|
188
|
+
.select({ prId: reviews.prId, authorId: reviews.authorId })
|
|
189
|
+
.from(reviews)
|
|
190
|
+
.where(and(inArray(reviews.prId, mergedIds), ne(reviews.state, 'pending')))
|
|
191
|
+
.execute(),
|
|
192
|
+
db
|
|
193
|
+
.select({ prId: reviewComments.prId, authorId: reviewComments.authorId })
|
|
194
|
+
.from(reviewComments)
|
|
195
|
+
.where(inArray(reviewComments.prId, mergedIds))
|
|
196
|
+
.execute(),
|
|
197
|
+
]);
|
|
198
|
+
const touched = new Set();
|
|
199
|
+
for (const r of [...reviewRows, ...commentRows]) {
|
|
200
|
+
if (lanes.laneOf(r.authorId) === 'human')
|
|
201
|
+
touched.add(r.prId);
|
|
202
|
+
}
|
|
203
|
+
mergedNoHuman = mergedIds.filter((id) => !touched.has(id)).length;
|
|
204
|
+
}
|
|
205
|
+
const metrics = [
|
|
206
|
+
countCell('prs_opened', opened),
|
|
207
|
+
countCell('prs_merged', merged),
|
|
208
|
+
countCell('prs_closed_unmerged', closedUnmerged),
|
|
209
|
+
cell('merge_rate_pct', mergeRate, resolved),
|
|
210
|
+
cell('median_hours_to_merge', hoursToMerge.length > 0 ? round2(median(hoursToMerge)) : null, hoursToMerge.length),
|
|
211
|
+
cell('median_pr_size_lines', sizes.length > 0 ? Math.round(median(sizes)) : null, sizes.length),
|
|
212
|
+
cell('prs_merged_without_human_review', mergedNoHuman, mergedIds.length),
|
|
213
|
+
countCell('human_review_comments_received', humanComments),
|
|
214
|
+
countCell('repos_touched', repoRows.length),
|
|
215
|
+
];
|
|
216
|
+
const roleByUser = await reviewerRoleForUser(accountId, scope.workspaceId);
|
|
217
|
+
const role = roleByUser.get(userId) ?? null;
|
|
218
|
+
const out = {
|
|
219
|
+
userId,
|
|
220
|
+
login: identity?.login ?? null,
|
|
221
|
+
displayName: identity?.name ?? null,
|
|
222
|
+
role,
|
|
223
|
+
repos: repoRows.slice(0, REPO_LIST_CAP).map((r) => ({
|
|
224
|
+
repoId: r.repoId,
|
|
225
|
+
repoFullName: `${r.owner}/${r.name}`,
|
|
226
|
+
prs: r.c,
|
|
227
|
+
})),
|
|
228
|
+
metrics,
|
|
229
|
+
};
|
|
230
|
+
if (opts.evidence === true) {
|
|
231
|
+
out.evidence = await loadAutomationEvidence(accountId, scope, userId, out.login, from, to, lanes);
|
|
232
|
+
}
|
|
233
|
+
return out;
|
|
234
|
+
}
|
|
235
|
+
/** The receipt rows. Each group is capped at PERSON_EVIDENCE_CAP with an honest remainder, and
|
|
236
|
+
* each is the SAME population as the metric it sits under — a card a reader can check the
|
|
237
|
+
* figure against, never a differently-filtered list that happens to look related. */
|
|
238
|
+
async function loadAutomationEvidence(accountId, scope, userId, subjectLogin, from, to, lanes) {
|
|
239
|
+
const inScope = and(eq(pullRequests.accountId, accountId), inArray(pullRequests.repoId, scope.repoIds));
|
|
240
|
+
const authored = and(inScope, eq(pullRequests.authorId, userId));
|
|
241
|
+
// Everything DigestPrRef renders, selected once. The table draws PR | CI | age | author | diff,
|
|
242
|
+
// so omitting `ciStatus`/`openedAt`/`changedFiles` leaves two columns as bare dashes on every
|
|
243
|
+
// row — the shape looks finished and reads as "no data" rather than "not selected".
|
|
244
|
+
const selectPr = {
|
|
245
|
+
prId: pullRequests.id,
|
|
246
|
+
prNumber: pullRequests.number,
|
|
247
|
+
repoId: pullRequests.repoId,
|
|
248
|
+
repoOwner: repos.owner,
|
|
249
|
+
repoName: repos.name,
|
|
250
|
+
title: pullRequests.title,
|
|
251
|
+
state: pullRequests.state,
|
|
252
|
+
ciStatus: pullRequests.ciStatus,
|
|
253
|
+
openedAt: pullRequests.openedAt,
|
|
254
|
+
additions: pullRequests.additions,
|
|
255
|
+
deletions: pullRequests.deletions,
|
|
256
|
+
changedFiles: pullRequests.changedFiles,
|
|
257
|
+
};
|
|
258
|
+
const [mergedRows, mergedTotal, closedRows, closedTotal, commentedRows] = await Promise.all([
|
|
259
|
+
db
|
|
260
|
+
.select(selectPr)
|
|
261
|
+
.from(pullRequests)
|
|
262
|
+
.innerJoin(repos, eq(repos.id, pullRequests.repoId))
|
|
263
|
+
.where(and(authored, gte(pullRequests.mergedAt, from), lt(pullRequests.mergedAt, to)))
|
|
264
|
+
.orderBy(desc(pullRequests.mergedAt))
|
|
265
|
+
.limit(PERSON_EVIDENCE_CAP)
|
|
266
|
+
.execute(),
|
|
267
|
+
db
|
|
268
|
+
.select({ c: count() })
|
|
269
|
+
.from(pullRequests)
|
|
270
|
+
.where(and(authored, gte(pullRequests.mergedAt, from), lt(pullRequests.mergedAt, to)))
|
|
271
|
+
.execute(),
|
|
272
|
+
db
|
|
273
|
+
.select(selectPr)
|
|
274
|
+
.from(pullRequests)
|
|
275
|
+
.innerJoin(repos, eq(repos.id, pullRequests.repoId))
|
|
276
|
+
.where(and(authored, isNull(pullRequests.mergedAt), gte(pullRequests.closedAt, from), lt(pullRequests.closedAt, to)))
|
|
277
|
+
.orderBy(desc(pullRequests.closedAt))
|
|
278
|
+
.limit(PERSON_EVIDENCE_CAP)
|
|
279
|
+
.execute(),
|
|
280
|
+
db
|
|
281
|
+
.select({ c: count() })
|
|
282
|
+
.from(pullRequests)
|
|
283
|
+
.where(and(authored, isNull(pullRequests.mergedAt), gte(pullRequests.closedAt, from), lt(pullRequests.closedAt, to)))
|
|
284
|
+
.execute(),
|
|
285
|
+
// PRs of its that drew review comments in the window, with the commenter so the human filter
|
|
286
|
+
// runs through the lane resolver rather than in SQL. Capped generously before folding, since
|
|
287
|
+
// the fold collapses many comments onto few PRs.
|
|
288
|
+
db
|
|
289
|
+
.select({
|
|
290
|
+
...selectPr,
|
|
291
|
+
commenterId: reviewComments.authorId,
|
|
292
|
+
commentedAt: reviewComments.createdAt,
|
|
293
|
+
})
|
|
294
|
+
.from(reviewComments)
|
|
295
|
+
.innerJoin(pullRequests, eq(pullRequests.id, reviewComments.prId))
|
|
296
|
+
.innerJoin(repos, eq(repos.id, pullRequests.repoId))
|
|
297
|
+
.where(and(authored, gte(reviewComments.createdAt, from), lt(reviewComments.createdAt, to)))
|
|
298
|
+
.orderBy(desc(reviewComments.createdAt))
|
|
299
|
+
.limit(PERSON_EVIDENCE_CAP * 20)
|
|
300
|
+
.execute(),
|
|
301
|
+
]);
|
|
302
|
+
// No cast: every DigestPrRef field is spelled out, so dropping one from `selectPr` becomes a
|
|
303
|
+
// compile error rather than a silently blank column.
|
|
304
|
+
const toRef = (r) => ({
|
|
305
|
+
prId: r.prId,
|
|
306
|
+
prNumber: r.prNumber,
|
|
307
|
+
repoId: r.repoId,
|
|
308
|
+
repoFullName: `${r.repoOwner}/${r.repoName}`,
|
|
309
|
+
title: r.title,
|
|
310
|
+
// The author is the subject of this whole vector, so the login is resolved once by the caller
|
|
311
|
+
// rather than re-joined per row; `authorId` is what the table renders an avatar from.
|
|
312
|
+
authorLogin: subjectLogin,
|
|
313
|
+
authorId: userId,
|
|
314
|
+
state: r.state,
|
|
315
|
+
ciStatus: r.ciStatus,
|
|
316
|
+
additions: r.additions,
|
|
317
|
+
deletions: r.deletions,
|
|
318
|
+
changedFiles: r.changedFiles,
|
|
319
|
+
openedAt: r.openedAt?.toISOString() ?? null,
|
|
320
|
+
});
|
|
321
|
+
// One row per PR, newest comment first, humans only.
|
|
322
|
+
const seen = new Set();
|
|
323
|
+
const humanReviewed = [];
|
|
324
|
+
let humanReviewedSeen = 0;
|
|
325
|
+
for (const r of commentedRows) {
|
|
326
|
+
if (lanes.laneOf(r.commenterId) !== 'human')
|
|
327
|
+
continue;
|
|
328
|
+
if (seen.has(r.prId))
|
|
329
|
+
continue;
|
|
330
|
+
seen.add(r.prId);
|
|
331
|
+
humanReviewedSeen += 1;
|
|
332
|
+
if (humanReviewed.length < PERSON_EVIDENCE_CAP)
|
|
333
|
+
humanReviewed.push(toRef(r));
|
|
334
|
+
}
|
|
335
|
+
const mergedCount = mergedTotal[0]?.c ?? 0;
|
|
336
|
+
const closedCount = closedTotal[0]?.c ?? 0;
|
|
337
|
+
return {
|
|
338
|
+
merged: mergedRows.map(toRef),
|
|
339
|
+
mergedMore: Math.max(0, mergedCount - mergedRows.length),
|
|
340
|
+
closedUnmerged: closedRows.map(toRef),
|
|
341
|
+
closedUnmergedMore: Math.max(0, closedCount - closedRows.length),
|
|
342
|
+
humanReviewed,
|
|
343
|
+
// The remainder here is over what the capped scan SAW, so it can under-report on a very
|
|
344
|
+
// chatty period. That is why the caption says "at least" — it must never claim to be the
|
|
345
|
+
// population figure the way the two above do.
|
|
346
|
+
humanReviewedMore: Math.max(0, humanReviewedSeen - humanReviewed.length),
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
//# sourceMappingURL=automation-output.js.map
|