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,634 @@
|
|
|
1
|
+
import { and, desc, eq, inArray } from 'drizzle-orm';
|
|
2
|
+
import { db, schema, runTransaction } from '../db/client.js';
|
|
3
|
+
import { TREND_DAYS } from '../db/branch-queries.js';
|
|
4
|
+
// The Feed's rolling read window, imported rather than re-stated: the trunk CI transition log's
|
|
5
|
+
// retention is defined BY it (see TRUNK_CI_EVENT_WINDOW), and two copies of "14" would drift
|
|
6
|
+
// into a trim that silently deletes rows the Feed still reads.
|
|
7
|
+
import { FEED_WINDOW_DAYS } from '../db/queries.js';
|
|
8
|
+
import { getGraphqlClientFor, graphqlTolerant, withGithubRetry, } from '../github/client.js';
|
|
9
|
+
import { gateBudget } from '../github/rate-budget.js';
|
|
10
|
+
import { buildCommitChecksQuery, COMMIT_CHECKS_ALIAS_CAP, DEFAULT_BRANCH_QUERY, DEFAULT_BRANCH_HISTORY_QUERY, } from '../github/branch-queries.js';
|
|
11
|
+
// One shared mapper: GitHub's `StatusState` enum is identical on a Commit's rollup and on a PR
|
|
12
|
+
// head's, so this reuses sync/upsert.ts's rather than keeping a second copy that could drift.
|
|
13
|
+
// `checkContextState` + `parseActionsIds` are shared for the same reason one level down: a
|
|
14
|
+
// failing check on trunk must be the same object as a failing check on a PR.
|
|
15
|
+
import { checkContextState, ciStatusFrom, parseActionsIds } from './upsert.js';
|
|
16
|
+
const { repos, users, branchCommits, trunkCiStatusEvents } = schema;
|
|
17
|
+
// How many trunk commits the LIVE snapshot fetches per repo — GitHub's page maximum — and the
|
|
18
|
+
// unconditional floor of the trim below (the newest this-many rows are never deleted, however
|
|
19
|
+
// old). Rows beyond it survive only inside the TREND_DAYS window; they come from the one-time
|
|
20
|
+
// history backfill (`backfillBranchHistory`), whose ciStatus is final by the time it runs, so
|
|
21
|
+
// the live walk not re-observing them is by design rather than a staleness hole.
|
|
22
|
+
//
|
|
23
|
+
// Cost of the widening (20 → 100): phase 1's node budget is N + 3N (the history page plus its
|
|
24
|
+
// nested associatedPullRequests(first: 3)), scored ceil(total/100) — so N=100 is 400 nodes ⇒
|
|
25
|
+
// 4 rate-limit points per repo per sync, where N=20 was 1. An ACCEPTED cost: the wider window is
|
|
26
|
+
// what feeds the branch-trends charts (90 days of trunk CI + what landed), and it rides the
|
|
27
|
+
// per-repo sync, not every adaptive tick.
|
|
28
|
+
export const BRANCH_COMMIT_WINDOW = 100;
|
|
29
|
+
// The trim is a HYBRID bound: the newest BRANCH_COMMIT_WINDOW rows are kept UNCONDITIONALLY,
|
|
30
|
+
// and rows below that survive as long as they sit inside the TREND_DAYS window (which is what
|
|
31
|
+
// lets the one-time history backfill's deeper rows live — a pure count bound would delete them
|
|
32
|
+
// on the very next sync tick). The unconditional half is a landmine guard, not a nicety: an
|
|
33
|
+
// age bound applied to ALL rows would mean a repo whose newest trunk commit is older than the
|
|
34
|
+
// cutoff (dormant repo, or an active one whose committer dates were backdated by a
|
|
35
|
+
// rebase/import) has its ENTIRE set inserted and deleted in the same transaction on every sync
|
|
36
|
+
// — the strip row reads "never synced", the expander is permanently disabled, and the walk
|
|
37
|
+
// burns its 4 points writing rows it immediately destroys. The trends read
|
|
38
|
+
// (db/branch-queries.ts getBranchTrends) applies its own `committedAt >= now - 90d` filter,
|
|
39
|
+
// which is where the "no year-old bars" promise actually lives.
|
|
40
|
+
// Which rollup states earn the SECOND round trip for failing-check detail. A green commit is
|
|
41
|
+
// skipped entirely — that is what keeps the common case at the ~1 point this file's query header
|
|
42
|
+
// promises. 'pending' is included because GitHub reports the ROLLUP as PENDING while other checks
|
|
43
|
+
// are still running even after one has already FAILED: that is the classic amber-with-a-real-
|
|
44
|
+
// failure case, and without it an amber commit could never get a caret.
|
|
45
|
+
const DETAIL_STATES = new Set(['failure', 'error', 'pending']);
|
|
46
|
+
// The rollup states that are a POSITIVE statement that nothing is failing, and therefore the only
|
|
47
|
+
// ones allowed to CLEAR stored detail. See the partial-response policy below for why 'unknown'
|
|
48
|
+
// (which is also what a nulled-by-partial-error rollup maps to) is deliberately not in here.
|
|
49
|
+
const CLEAR_STATES = new Set(['success', 'expected']);
|
|
50
|
+
// Defensive cap on the stored array. The whole point of this column is that it is tiny; a
|
|
51
|
+
// pathological repo with 300 failing contexts must not write a fat row per commit.
|
|
52
|
+
export const MAX_FAILING_CHECKS_PER_COMMIT = 20;
|
|
53
|
+
const DAY_MS = 86_400_000;
|
|
54
|
+
// How many pages the one-time history backfill will walk (at BRANCH_COMMIT_WINDOW commits and
|
|
55
|
+
// ~4 rate-limit points each). 10 pages = 1000 trunk commits — deeper than 90 days of all but
|
|
56
|
+
// the busiest repos; on those the strip's oldest days simply stay unpopulated, disclosed by
|
|
57
|
+
// the `truncated` flag in the result (and the caller's log line), never silently.
|
|
58
|
+
export const BRANCH_BACKFILL_MAX_PAGES = 10;
|
|
59
|
+
// ---- The partial-response policy, applied identically to BOTH new columns ------------------
|
|
60
|
+
//
|
|
61
|
+
// `graphqlTolerant` returns GitHub's PARTIAL data when a token may read most of a query but is
|
|
62
|
+
// forbidden one sub-field — the forbidden field arrives NULLED. So "the response says null" and
|
|
63
|
+
// "we never received that selection" look the same at the JSON level, and the two demand opposite
|
|
64
|
+
// writes: the first should clear the column, the second must leave it alone. Writing
|
|
65
|
+
// unconditionally would NULL yesterday's good detail on every tick for such a token, and a red
|
|
66
|
+
// commit would permanently lose its caret (and a chip would blink out) for a reason no user can
|
|
67
|
+
// see. Writing conditionally on non-null would instead leave a stale value alive forever after a
|
|
68
|
+
// re-run turned a check green.
|
|
69
|
+
//
|
|
70
|
+
// The resolution, used by both `failingChecksToWrite` and `prNumberToWrite`: a value is written
|
|
71
|
+
// ONLY when the response actually CARRIED the selection it is derived from —
|
|
72
|
+
// • `undefined` ⇒ not received / not knowable ⇒ omit the column from the write entirely.
|
|
73
|
+
// • `null` / `[]` ⇒ GitHub positively stated there is nothing ⇒ write it, clearing any
|
|
74
|
+
// previously stored value.
|
|
75
|
+
// For failing checks the positive statement is a green (or 'expected') ROLLUP from phase 1, or a
|
|
76
|
+
// phase-2 response that actually contained a `contexts` list. For the PR reference it is an
|
|
77
|
+
// `associatedPullRequests.nodes` ARRAY (an empty one means "this commit came from no PR" — a
|
|
78
|
+
// direct push, which is a legitimate steady state, not a gap).
|
|
79
|
+
/**
|
|
80
|
+
* The FAILING checks among a commit's rollup contexts — never the passing ones.
|
|
81
|
+
*
|
|
82
|
+
* Reuses the PR side's `checkContextState` + `parseActionsIds` verbatim so a trunk failure and a
|
|
83
|
+
* PR failure are the same object, then dedupes by display name keeping the newest Actions run:
|
|
84
|
+
* `contexts` returns EVERY check suite attached to the commit and does NOT collapse to
|
|
85
|
+
* latest-per-name the way GitHub's own PR UI does (a re-run, or a workflow fired by both `push`
|
|
86
|
+
* and `pull_request`, contributes a second same-named CheckRun). Actions run ids increase
|
|
87
|
+
* monotonically, so the highest runId is the most recent.
|
|
88
|
+
*/
|
|
89
|
+
export function failingChecksFrom(nodes) {
|
|
90
|
+
const byName = new Map();
|
|
91
|
+
for (const c of nodes) {
|
|
92
|
+
if (c == null)
|
|
93
|
+
continue;
|
|
94
|
+
const state = checkContextState(c);
|
|
95
|
+
if (state !== 'failure' && state !== 'error')
|
|
96
|
+
continue;
|
|
97
|
+
const url = c.__typename === 'CheckRun' ? c.detailsUrl : c.targetUrl;
|
|
98
|
+
const { runId, jobId } = c.__typename === 'CheckRun' ? parseActionsIds(url) : { runId: null, jobId: null };
|
|
99
|
+
const item = {
|
|
100
|
+
// Third-party text (a check's name is whatever the CI vendor chose), so it is length-capped
|
|
101
|
+
// here rather than trusted; the frontend renders it as a plain text node.
|
|
102
|
+
name: (c.__typename === 'CheckRun' ? c.name : c.context).slice(0, 200),
|
|
103
|
+
state,
|
|
104
|
+
url,
|
|
105
|
+
runId,
|
|
106
|
+
jobId,
|
|
107
|
+
workflowName: c.__typename === 'CheckRun'
|
|
108
|
+
? (c.checkSuite?.workflowRun?.workflow?.name ?? null)
|
|
109
|
+
: null,
|
|
110
|
+
};
|
|
111
|
+
const prev = byName.get(item.name);
|
|
112
|
+
if (!prev || (item.runId ?? -1) > (prev.runId ?? -1))
|
|
113
|
+
byName.set(item.name, item);
|
|
114
|
+
}
|
|
115
|
+
return [...byName.values()].slice(0, MAX_FAILING_CHECKS_PER_COMMIT);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Which single PR number to store for a trunk commit, or null for a direct push.
|
|
119
|
+
*
|
|
120
|
+
* The 0 / 1 / many contract:
|
|
121
|
+
* 0 candidates → a direct push to trunk (the legitimate no-chip case) → null.
|
|
122
|
+
* 1 → the normal squash/merge landing → that number.
|
|
123
|
+
* 2+ → the commit is reachable from several PRs (a branch merged into another open
|
|
124
|
+
* PR, a cherry-pick, a revert-then-reland). Store exactly ONE, ranked
|
|
125
|
+
* (merged into THIS repo's default branch) > (merged anywhere) > (open), with
|
|
126
|
+
* the LOWEST number as a stable tiebreak.
|
|
127
|
+
*
|
|
128
|
+
* Determinism is the whole point: the ranking is ours precisely so a re-sync can never flip the
|
|
129
|
+
* displayed number, which `first: 1` on an unordered connection would.
|
|
130
|
+
*
|
|
131
|
+
* A candidate from another repository is DROPPED: `associatedPullRequests` spans the repo network,
|
|
132
|
+
* so a fork's own PR can appear, and the read layer resolves a number within (accountId, repoId)
|
|
133
|
+
* — a foreign number would resolve to a completely unrelated local PR. A null `nameWithOwner` (a
|
|
134
|
+
* tolerant partial nulled it) is accepted rather than discarding an otherwise good candidate.
|
|
135
|
+
*/
|
|
136
|
+
export function pickAssociatedPrNumber(n, fullName, defaultBranchName) {
|
|
137
|
+
const cands = (n.associatedPullRequests?.nodes ?? []).filter((p) => p != null &&
|
|
138
|
+
typeof p.number === 'number' &&
|
|
139
|
+
(p.repository?.nameWithOwner == null || p.repository.nameWithOwner === fullName));
|
|
140
|
+
const first = cands[0];
|
|
141
|
+
if (first == null)
|
|
142
|
+
return null;
|
|
143
|
+
const rank = (p) => {
|
|
144
|
+
if (p.merged !== true)
|
|
145
|
+
return 2;
|
|
146
|
+
return defaultBranchName != null && p.baseRefName === defaultBranchName ? 0 : 1;
|
|
147
|
+
};
|
|
148
|
+
let best = first;
|
|
149
|
+
for (const p of cands.slice(1)) {
|
|
150
|
+
const d = rank(p) - rank(best);
|
|
151
|
+
if (d < 0 || (d === 0 && p.number < best.number))
|
|
152
|
+
best = p;
|
|
153
|
+
}
|
|
154
|
+
return best.number;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Which commits in the window earn a phase-2 detail fetch: the non-green ones, newest first.
|
|
158
|
+
*
|
|
159
|
+
* `history()` returns newest-first, so slicing to the alias cap keeps the NEWEST failing commits —
|
|
160
|
+
* the ones anyone will actually expand. Exported so the "a green trunk issues no second query"
|
|
161
|
+
* claim is a test rather than a comment.
|
|
162
|
+
*/
|
|
163
|
+
export function detailTargetShas(nodes) {
|
|
164
|
+
return nodes
|
|
165
|
+
.filter((n) => DETAIL_STATES.has(ciStatusFrom(n.statusCheckRollup?.state)))
|
|
166
|
+
.slice(0, COMMIT_CHECKS_ALIAS_CAP)
|
|
167
|
+
.map((n) => n.oid);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* The failing-checks value to WRITE for one commit, per the partial-response policy above:
|
|
171
|
+
* `undefined` ⇒ omit the column (we did not learn anything), `null` ⇒ positively no failures.
|
|
172
|
+
*/
|
|
173
|
+
export function failingChecksToWrite(n, failingBySha) {
|
|
174
|
+
const rollup = ciStatusFrom(n.statusCheckRollup?.state);
|
|
175
|
+
if (CLEAR_STATES.has(rollup))
|
|
176
|
+
return null;
|
|
177
|
+
// 'unknown' — either no rollup was received (a partial error nulled it) or GitHub's state isn't
|
|
178
|
+
// one we model. Neither is a statement that nothing is failing, so nothing is cleared.
|
|
179
|
+
if (!DETAIL_STATES.has(rollup))
|
|
180
|
+
return undefined;
|
|
181
|
+
const found = failingBySha.get(n.oid);
|
|
182
|
+
// Absent ⇒ past the alias cap, or the contexts selection wasn't received: keep what we have.
|
|
183
|
+
if (found == null)
|
|
184
|
+
return undefined;
|
|
185
|
+
return found.length > 0 ? found : null;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* The PR-number value to WRITE for one commit, per the same policy. An `associatedPullRequests`
|
|
189
|
+
* that arrived as null (a partial error) yields `undefined` — writing null there would erase a good
|
|
190
|
+
* stored number and make the chip blink out of the UI for a reason the user cannot see.
|
|
191
|
+
*/
|
|
192
|
+
export function prNumberToWrite(n, fullName, defaultBranchName) {
|
|
193
|
+
return Array.isArray(n.associatedPullRequests?.nodes)
|
|
194
|
+
? pickAssociatedPrNumber(n, fullName, defaultBranchName)
|
|
195
|
+
: undefined;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Phase 2: the failing checks for the given shas, in ONE query.
|
|
199
|
+
*
|
|
200
|
+
* The returned map contains an entry ONLY for a sha whose `contexts` list actually arrived — an
|
|
201
|
+
* absent key means "not received", which the caller must not confuse with "no failures". See the
|
|
202
|
+
* partial-response policy above. Exported for the request-shape unit test.
|
|
203
|
+
*/
|
|
204
|
+
export async function fetchFailingChecks(client, owner, name, shas, log) {
|
|
205
|
+
const variables = { owner, name };
|
|
206
|
+
shas.forEach((sha, i) => {
|
|
207
|
+
variables[`s${i}`] = sha;
|
|
208
|
+
});
|
|
209
|
+
const resp = await withGithubRetry(() => graphqlTolerant(client, buildCommitChecksQuery(shas.length), variables, (errors) => log?.warn(`branch-status ${owner}/${name}: partial GraphQL on commit checks — ` +
|
|
210
|
+
`keeping previously stored detail for the affected commits`, errors)));
|
|
211
|
+
const bySha = new Map();
|
|
212
|
+
shas.forEach((sha, i) => {
|
|
213
|
+
const nodes = resp.repository?.[`c${i}`]?.statusCheckRollup?.contexts?.nodes;
|
|
214
|
+
// `nodes == null` covers all three not-received shapes: the alias resolved to nothing, the
|
|
215
|
+
// rollup was nulled, or the contexts selection itself was forbidden.
|
|
216
|
+
if (nodes == null)
|
|
217
|
+
return;
|
|
218
|
+
bySha.set(sha, failingChecksFrom(nodes));
|
|
219
|
+
});
|
|
220
|
+
return { bySha, cost: resp.rateLimit?.cost ?? 0 };
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* The commit-author logins we already know, resolved in ONE query. Lookup-only on purpose —
|
|
224
|
+
* see the author-mapping note on `syncBranchStatus`: no `users` rows are ever created here.
|
|
225
|
+
*/
|
|
226
|
+
async function resolveKnownLogins(nodes) {
|
|
227
|
+
const logins = [
|
|
228
|
+
...new Set(nodes
|
|
229
|
+
.map((n) => n.author?.user?.login)
|
|
230
|
+
.filter((l) => typeof l === 'string' && l.length > 0)),
|
|
231
|
+
];
|
|
232
|
+
const userIdByLogin = new Map();
|
|
233
|
+
if (logins.length > 0) {
|
|
234
|
+
const rows = await db
|
|
235
|
+
.select({ id: users.id, login: users.githubLogin })
|
|
236
|
+
.from(users)
|
|
237
|
+
.where(inArray(users.githubLogin, logins))
|
|
238
|
+
.execute();
|
|
239
|
+
for (const r of rows)
|
|
240
|
+
userIdByLogin.set(r.login, r.id);
|
|
241
|
+
}
|
|
242
|
+
return userIdByLogin;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Upsert one page of trunk-commit nodes — shared verbatim between the live snapshot and the
|
|
246
|
+
* history backfill so the two walks can never store differently-shaped rows. Idempotent on
|
|
247
|
+
* `(accountId, repoId, sha)`; the observed-key spread implements the partial-response policy
|
|
248
|
+
* above (an omitted key leaves the column at its default on INSERT and untouched on UPDATE,
|
|
249
|
+
* which is exactly what "we did not receive that selection" means).
|
|
250
|
+
*/
|
|
251
|
+
async function upsertBranchCommitRows(tx, args, nodes, userIdByLogin, failingBySha) {
|
|
252
|
+
for (const n of nodes) {
|
|
253
|
+
const login = n.author?.user?.login ?? null;
|
|
254
|
+
const authorUserId = login != null ? userIdByLogin.get(login) ?? null : null;
|
|
255
|
+
const failingChecks = failingChecksToWrite(n, failingBySha);
|
|
256
|
+
const prNumber = prNumberToWrite(n, args.fullName, args.branchName);
|
|
257
|
+
const observed = {
|
|
258
|
+
...(failingChecks !== undefined ? { failingChecks } : {}),
|
|
259
|
+
...(prNumber !== undefined ? { prNumber } : {}),
|
|
260
|
+
};
|
|
261
|
+
await tx
|
|
262
|
+
.insert(branchCommits)
|
|
263
|
+
.values({
|
|
264
|
+
accountId: args.accountId,
|
|
265
|
+
repoId: args.repoId,
|
|
266
|
+
sha: n.oid,
|
|
267
|
+
messageHeadline: n.messageHeadline,
|
|
268
|
+
authorUserId,
|
|
269
|
+
// Prefer the git author name; fall back to the login so a commit by a known GitHub
|
|
270
|
+
// account with a blank git name still shows SOMETHING attributable.
|
|
271
|
+
authorName: n.author?.name ?? login,
|
|
272
|
+
authorAvatarUrl: n.author?.avatarUrl ?? null,
|
|
273
|
+
committedAt: new Date(n.committedDate),
|
|
274
|
+
ciStatus: ciStatusFrom(n.statusCheckRollup?.state),
|
|
275
|
+
...observed,
|
|
276
|
+
})
|
|
277
|
+
.onConflictDoUpdate({
|
|
278
|
+
target: [branchCommits.accountId, branchCommits.repoId, branchCommits.sha],
|
|
279
|
+
set: {
|
|
280
|
+
messageHeadline: n.messageHeadline,
|
|
281
|
+
authorUserId,
|
|
282
|
+
authorName: n.author?.name ?? login,
|
|
283
|
+
authorAvatarUrl: n.author?.avatarUrl ?? null,
|
|
284
|
+
committedAt: new Date(n.committedDate),
|
|
285
|
+
ciStatus: ciStatusFrom(n.statusCheckRollup?.state),
|
|
286
|
+
...observed,
|
|
287
|
+
},
|
|
288
|
+
})
|
|
289
|
+
.execute();
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Which of the retained rows the trim deletes, given rows sorted NEWEST-FIRST: anything both
|
|
294
|
+
* below the newest-BRANCH_COMMIT_WINDOW floor AND outside the TREND_DAYS window. Pure and
|
|
295
|
+
* exported so the hybrid bound's two halves are pinned by tests rather than a comment.
|
|
296
|
+
*/
|
|
297
|
+
export function staleBranchCommitIds(rows, nowMs) {
|
|
298
|
+
const cutoffMs = nowMs - TREND_DAYS * DAY_MS;
|
|
299
|
+
return rows
|
|
300
|
+
.filter((r, i) => i >= BRANCH_COMMIT_WINDOW && r.committedAt.getTime() < cutoffMs)
|
|
301
|
+
.map((r) => r.id);
|
|
302
|
+
}
|
|
303
|
+
// ---- The trunk CI TRANSITION log (trunk_ci_status_events) ----------------------------------
|
|
304
|
+
//
|
|
305
|
+
// `branch_commits.ci_status` is updated IN PLACE by the upsert above, so the moment trunk turned
|
|
306
|
+
// red is nowhere on that row — its only timestamps are `committedAt` (git commit time) and
|
|
307
|
+
// `createdAt` (first insertion). Approximating a failure time with either would be a quiet lie,
|
|
308
|
+
// so an observation gets its own append-only row, exactly as `ci_status_events` does for a PR
|
|
309
|
+
// head (sync/upsert.ts). It is what makes the Feed's trunk CI-failure cards honest.
|
|
310
|
+
//
|
|
311
|
+
// How many rows one repo's log keeps UNCONDITIONALLY. The trim is HYBRID, exactly like
|
|
312
|
+
// `branch_commits` above (newest-N ∪ inside the read window), and for the same two reasons in
|
|
313
|
+
// opposite directions:
|
|
314
|
+
//
|
|
315
|
+
// • The count half alone was WRONG. `trunkCiTransitionChanged` records a transition on any head
|
|
316
|
+
// move, and `syncBranchStatus` runs at the end of every walk (as often as every 120s on a hot
|
|
317
|
+
// repo), so an active repo produces transitions far faster than the Feed's window elapses —
|
|
318
|
+
// and a pure newest-200 bound evicted the failure rows `getTrunkCiFailureFeedItems` reads over
|
|
319
|
+
// FEED_WINDOW_DAYS. The symptom is invisible: the Feed just quietly stops showing trunk
|
|
320
|
+
// failures on exactly the repos that have the most of them.
|
|
321
|
+
// • The age half alone would be wrong too. A dormant repo's whole log is older than the cutoff,
|
|
322
|
+
// so an unconditional age bound would delete every row it has and the next observation would
|
|
323
|
+
// read as a first observation forever.
|
|
324
|
+
export const TRUNK_CI_EVENT_WINDOW = 200;
|
|
325
|
+
/**
|
|
326
|
+
* Which of a repo's trunk CI transition rows the trim deletes, given rows sorted NEWEST-FIRST:
|
|
327
|
+
* anything both below the newest-TRUNK_CI_EVENT_WINDOW floor AND older than the Feed's read
|
|
328
|
+
* window. Pure and exported so the hybrid bound's two halves are pinned by tests, not comments.
|
|
329
|
+
*
|
|
330
|
+
* `observedAt` (our observation time) is the right axis because it is exactly what
|
|
331
|
+
* `getTrunkCiFailureFeedItems` filters on.
|
|
332
|
+
*/
|
|
333
|
+
export function staleTrunkCiEventIds(rows, nowMs) {
|
|
334
|
+
const cutoffMs = nowMs - FEED_WINDOW_DAYS * DAY_MS;
|
|
335
|
+
return rows
|
|
336
|
+
.filter((r, i) => i >= TRUNK_CI_EVENT_WINDOW && r.observedAt.getTime() < cutoffMs)
|
|
337
|
+
.map((r) => r.id);
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Whether a fresh observation is a TRANSITION worth recording, given this repo's last row.
|
|
341
|
+
*
|
|
342
|
+
* Mirrors the PR-side predicate in `sync/upsert.ts` (status / head sha / failing-check name set)
|
|
343
|
+
* with TWO deliberate differences:
|
|
344
|
+
*
|
|
345
|
+
* • `failingCheckNames` may be `undefined`, meaning "phase 2 never told us which checks failed".
|
|
346
|
+
* Per the partial-response policy at the top of this file, that is not a statement that the set
|
|
347
|
+
* changed, so the name dimension is simply DROPPED from the comparison rather than compared
|
|
348
|
+
* against an empty list — otherwise a repo whose detail fetch fails would log a spurious
|
|
349
|
+
* "checks changed" transition on every single sync.
|
|
350
|
+
* • A head move on a GREEN trunk is not a transition. Trunk's head changes on every landed PR,
|
|
351
|
+
* so recording each one filled an active repo's log with rows that state nothing (the Feed
|
|
352
|
+
* reads failure/error rows only) and pushed real failures out of it. The narrowing is
|
|
353
|
+
* deliberately minimal: it applies ONLY when the rollup is a positive "nothing is failing"
|
|
354
|
+
* (CLEAR_STATES) on BOTH sides with no named failures on either, so no red — or amber-with-a-
|
|
355
|
+
* real-failure — observation can be swallowed. A new head that is still red IS a new failure
|
|
356
|
+
* (its own card, on its own commit) and is always recorded, and the first green after a red is
|
|
357
|
+
* a status change, so recovery timing survives.
|
|
358
|
+
*
|
|
359
|
+
* Pure + exported so all of that is pinned by tests rather than a comment.
|
|
360
|
+
*/
|
|
361
|
+
export function trunkCiTransitionChanged(last, next) {
|
|
362
|
+
if (last == null)
|
|
363
|
+
return true;
|
|
364
|
+
if (last.status !== next.status)
|
|
365
|
+
return true;
|
|
366
|
+
const before = [...new Set((last.failingChecks ?? []).map((c) => c.name))].sort();
|
|
367
|
+
if (next.failingCheckNames !== undefined &&
|
|
368
|
+
JSON.stringify(before) !== JSON.stringify(next.failingCheckNames)) {
|
|
369
|
+
return true;
|
|
370
|
+
}
|
|
371
|
+
if (last.headSha === next.headSha)
|
|
372
|
+
return false;
|
|
373
|
+
// Head moved. Record it unless BOTH observations are a positive green with nothing named.
|
|
374
|
+
const stillClean = CLEAR_STATES.has(next.status) &&
|
|
375
|
+
before.length === 0 &&
|
|
376
|
+
(next.failingCheckNames?.length ?? 0) === 0;
|
|
377
|
+
return !stillClean;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Append one trunk CI observation, if it is a transition — then trim the repo's log.
|
|
381
|
+
*
|
|
382
|
+
* TWO GATES, both load-bearing:
|
|
383
|
+
* • `headSha == null` — the response never positively identified a branch head, so there is
|
|
384
|
+
* nothing to attribute an observation to.
|
|
385
|
+
* • `status === 'unknown'` — GitHub's rollup states we model are success/failure/pending/
|
|
386
|
+
* error/expected; 'unknown' is BOTH "a state we don't model" AND what `graphqlTolerant`
|
|
387
|
+
* yields when a partial response NULLs the rollup. Recording it would manufacture a
|
|
388
|
+
* "trunk changed" event out of a permissions error. (Same gate the PR-side writer uses.)
|
|
389
|
+
*
|
|
390
|
+
* Never throws: every caller treats the branch snapshot as strictly non-fatal, and a feed
|
|
391
|
+
* nicety must not be able to cost a repo its sync. Runs OUTSIDE the snapshot's transaction for
|
|
392
|
+
* the same reason — a failure here rolls back nothing.
|
|
393
|
+
*/
|
|
394
|
+
async function recordTrunkCiTransition(args) {
|
|
395
|
+
const { accountId, repoId, headSha, status } = args;
|
|
396
|
+
if (headSha == null || status === 'unknown')
|
|
397
|
+
return;
|
|
398
|
+
const failingCheckNames = args.failingChecks === undefined
|
|
399
|
+
? undefined
|
|
400
|
+
: [...new Set(args.failingChecks?.map((c) => c.name) ?? [])].sort();
|
|
401
|
+
const [last] = await db
|
|
402
|
+
.select({
|
|
403
|
+
status: trunkCiStatusEvents.status,
|
|
404
|
+
headSha: trunkCiStatusEvents.headSha,
|
|
405
|
+
failingChecks: trunkCiStatusEvents.failingChecks,
|
|
406
|
+
})
|
|
407
|
+
.from(trunkCiStatusEvents)
|
|
408
|
+
.where(and(eq(trunkCiStatusEvents.accountId, accountId), eq(trunkCiStatusEvents.repoId, repoId)))
|
|
409
|
+
// `id` breaks a same-instant tie deterministically (autoincrement ⇒ insertion order).
|
|
410
|
+
.orderBy(desc(trunkCiStatusEvents.observedAt), desc(trunkCiStatusEvents.id))
|
|
411
|
+
.limit(1)
|
|
412
|
+
.execute();
|
|
413
|
+
if (!trunkCiTransitionChanged(last ?? null, { status, headSha, failingCheckNames }))
|
|
414
|
+
return;
|
|
415
|
+
await db
|
|
416
|
+
.insert(trunkCiStatusEvents)
|
|
417
|
+
.values({
|
|
418
|
+
accountId,
|
|
419
|
+
repoId,
|
|
420
|
+
branchName: args.branchName,
|
|
421
|
+
headSha,
|
|
422
|
+
status,
|
|
423
|
+
// `undefined` names ⇒ store null. NOT inheriting the previous row's list: this is an
|
|
424
|
+
// append-only observation log, and copying names forward would fabricate evidence.
|
|
425
|
+
failingChecks: args.failingChecks ?? null,
|
|
426
|
+
observedAt: new Date(),
|
|
427
|
+
})
|
|
428
|
+
.execute();
|
|
429
|
+
// HYBRID trim (see TRUNK_CI_EVENT_WINDOW + staleTrunkCiEventIds): the newest N rows
|
|
430
|
+
// unconditionally, plus everything still inside the Feed's read window. select-then-
|
|
431
|
+
// delete-by-id rather than a correlated DELETE … LIMIT, which is not portable across sqlite
|
|
432
|
+
// and Postgres.
|
|
433
|
+
const kept = await db
|
|
434
|
+
.select({ id: trunkCiStatusEvents.id, observedAt: trunkCiStatusEvents.observedAt })
|
|
435
|
+
.from(trunkCiStatusEvents)
|
|
436
|
+
.where(and(eq(trunkCiStatusEvents.accountId, accountId), eq(trunkCiStatusEvents.repoId, repoId)))
|
|
437
|
+
.orderBy(desc(trunkCiStatusEvents.observedAt), desc(trunkCiStatusEvents.id))
|
|
438
|
+
.execute();
|
|
439
|
+
const stale = staleTrunkCiEventIds(kept, Date.now());
|
|
440
|
+
// `inArray(col, [])` is a degenerate predicate whose behaviour differs by dialect, and there
|
|
441
|
+
// is nothing to delete anyway.
|
|
442
|
+
if (stale.length > 0) {
|
|
443
|
+
await db.delete(trunkCiStatusEvents).where(inArray(trunkCiStatusEvents.id, stale)).execute();
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Snapshot a repo's DEFAULT BRANCH: its head sha + CI rollup onto `repos`, and the most recent
|
|
448
|
+
* `BRANCH_COMMIT_WINDOW` trunk commits (with their own per-commit CI state, the checks that were
|
|
449
|
+
* failing on them, and the PR each landed from) into `branch_commits`.
|
|
450
|
+
*
|
|
451
|
+
* Why this can't come from the existing tables: `commits` is PR-scoped, and a squash-merged PR
|
|
452
|
+
* never appears there under the SHA that actually landed on trunk. So "is main green, and what
|
|
453
|
+
* landed on it" is unanswerable without its own read.
|
|
454
|
+
*
|
|
455
|
+
* Idempotent — commits upsert on `(accountId, repoId, sha)`, so a re-sync of the same window
|
|
456
|
+
* updates each commit's CI state in place (a commit that was `pending` last tick becomes
|
|
457
|
+
* `success` on this one) rather than duplicating rows.
|
|
458
|
+
*
|
|
459
|
+
* Author mapping is deliberately LOOKUP-ONLY: a trunk committer who resolves to an already-known
|
|
460
|
+
* `users` row gets `authorUserId` (so the UI can render their synced avatar + the maintainer
|
|
461
|
+
* shield); anyone else is stored as raw `authorName`/`authorAvatarUrl`. We do NOT insert users
|
|
462
|
+
* here — `users` is a GLOBAL table, and creating rows for every drive-by trunk committer would
|
|
463
|
+
* grow it with identities that appear nowhere else in the account's data.
|
|
464
|
+
*/
|
|
465
|
+
export async function syncBranchStatus(opts) {
|
|
466
|
+
const { owner, name, repoId, accountId, token } = opts;
|
|
467
|
+
const client = getGraphqlClientFor(token);
|
|
468
|
+
const fullName = `${owner}/${name}`;
|
|
469
|
+
const resp = await withGithubRetry(() => graphqlTolerant(client, DEFAULT_BRANCH_QUERY, { owner, name, first: BRANCH_COMMIT_WINDOW }, (errors) => opts.log?.warn(`branch-status ${fullName}: partial GraphQL — continuing without forbidden fields`, errors)));
|
|
470
|
+
const ref = resp.repository?.defaultBranchRef ?? null;
|
|
471
|
+
const branchName = ref?.name ?? null;
|
|
472
|
+
// `target` is a nullable INTERFACE — a branch whose tip isn't a Commit selects nothing, so
|
|
473
|
+
// every field below is optional even when `target` itself is present.
|
|
474
|
+
const target = ref?.target ?? null;
|
|
475
|
+
const headSha = target?.oid ?? null;
|
|
476
|
+
const headCi = ciStatusFrom(target?.statusCheckRollup?.state);
|
|
477
|
+
const nodes = (target?.history?.nodes ?? []).filter((n) => n != null && typeof n.oid === 'string');
|
|
478
|
+
// Resolve the commit authors we already know, in ONE query. GraphQL gives a login only when
|
|
479
|
+
// the commit email maps to a GitHub account; the rest keep their raw git name.
|
|
480
|
+
const userIdByLogin = await resolveKnownLogins(nodes);
|
|
481
|
+
// Phase 2, for the non-green commits only — a green trunk issues no second query at all.
|
|
482
|
+
const detailTargets = detailTargetShas(nodes);
|
|
483
|
+
let failingBySha = new Map();
|
|
484
|
+
let detailCost = 0;
|
|
485
|
+
if (detailTargets.length > 0) {
|
|
486
|
+
// LOAD-BEARING try/catch: sync-repo.ts already treats the whole of `syncBranchStatus` as
|
|
487
|
+
// non-fatal, so an unguarded throw here would discard the phase-1 snapshot too — dots,
|
|
488
|
+
// headSha, authors, the entire strip for this repo. Detail failure must degrade to "no
|
|
489
|
+
// carets", never to "no strip".
|
|
490
|
+
try {
|
|
491
|
+
const r = await fetchFailingChecks(client, owner, name, detailTargets, opts.log);
|
|
492
|
+
failingBySha = r.bySha;
|
|
493
|
+
detailCost = r.cost;
|
|
494
|
+
}
|
|
495
|
+
catch (err) {
|
|
496
|
+
opts.log?.warn(`branch-status ${fullName}: failing-check detail failed (non-fatal): ` +
|
|
497
|
+
`${err instanceof Error ? err.message.slice(0, 200) : String(err)}`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
await runTransaction(async (tx) => {
|
|
501
|
+
await upsertBranchCommitRows(tx, { accountId, repoId, fullName, branchName }, nodes, userIdByLogin, failingBySha);
|
|
502
|
+
// Trim IN the same transaction, so the table can never be observed holding an unbounded
|
|
503
|
+
// history. HYBRID bound (see staleBranchCommitIds + the constants' comments): the newest
|
|
504
|
+
// BRANCH_COMMIT_WINDOW rows unconditionally, plus backfilled rows inside the TREND_DAYS
|
|
505
|
+
// window. Done as select-then-delete-by-id rather than a correlated DELETE … LIMIT, which
|
|
506
|
+
// is not portable across sqlite and Postgres.
|
|
507
|
+
const kept = await tx
|
|
508
|
+
.select({ id: branchCommits.id, committedAt: branchCommits.committedAt })
|
|
509
|
+
.from(branchCommits)
|
|
510
|
+
.where(and(eq(branchCommits.accountId, accountId), eq(branchCommits.repoId, repoId)))
|
|
511
|
+
.orderBy(desc(branchCommits.committedAt))
|
|
512
|
+
.execute();
|
|
513
|
+
const stale = staleBranchCommitIds(kept, Date.now());
|
|
514
|
+
// Guard the empty case: `inArray(col, [])` is a degenerate predicate whose behaviour
|
|
515
|
+
// differs by dialect, and there is nothing to delete anyway.
|
|
516
|
+
if (stale.length > 0) {
|
|
517
|
+
await tx.delete(branchCommits).where(inArray(branchCommits.id, stale)).execute();
|
|
518
|
+
}
|
|
519
|
+
await tx
|
|
520
|
+
.update(repos)
|
|
521
|
+
.set({
|
|
522
|
+
defaultBranchName: branchName,
|
|
523
|
+
defaultBranchHeadSha: headSha,
|
|
524
|
+
defaultBranchCiStatus: headCi,
|
|
525
|
+
defaultBranchUpdatedAt: new Date(),
|
|
526
|
+
})
|
|
527
|
+
// Ownership is part of the predicate, not an assumption about the caller.
|
|
528
|
+
.where(and(eq(repos.id, repoId), eq(repos.accountId, accountId)))
|
|
529
|
+
.execute();
|
|
530
|
+
});
|
|
531
|
+
// The trunk CI transition log — AFTER the snapshot's transaction and inside its own guard, so
|
|
532
|
+
// this feed nicety can never roll back (or fail) the strip that just succeeded. The failing
|
|
533
|
+
// checks handed over come from the head COMMIT's own node under the same partial-response
|
|
534
|
+
// policy as the column write (`undefined` = we did not learn them).
|
|
535
|
+
try {
|
|
536
|
+
const headNode = headSha != null ? nodes.find((n) => n.oid === headSha) : undefined;
|
|
537
|
+
await recordTrunkCiTransition({
|
|
538
|
+
accountId,
|
|
539
|
+
repoId,
|
|
540
|
+
fullName,
|
|
541
|
+
branchName,
|
|
542
|
+
headSha,
|
|
543
|
+
status: headCi,
|
|
544
|
+
failingChecks: headNode != null ? failingChecksToWrite(headNode, failingBySha) : undefined,
|
|
545
|
+
log: opts.log,
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
catch (err) {
|
|
549
|
+
opts.log?.warn(`branch-status ${fullName}: trunk CI transition log failed (non-fatal): ` +
|
|
550
|
+
`${err instanceof Error ? err.message.slice(0, 200) : String(err)}`);
|
|
551
|
+
}
|
|
552
|
+
const rateLimitCost = (resp.rateLimit?.cost ?? 0) + detailCost;
|
|
553
|
+
if (detailTargets.length > 0) {
|
|
554
|
+
// The one place the two-phase cost claim is observable. Logged only when phase 2 actually
|
|
555
|
+
// ran, so a healthy repo stays silent.
|
|
556
|
+
opts.log?.info(`branch-status ${fullName}: ${detailTargets.length} non-green commit(s), ` +
|
|
557
|
+
`${failingBySha.size} with retrievable checks, ${rateLimitCost} rate-limit point(s)`);
|
|
558
|
+
}
|
|
559
|
+
return {
|
|
560
|
+
branchName,
|
|
561
|
+
headSha,
|
|
562
|
+
ciStatus: headCi,
|
|
563
|
+
commitCount: nodes.length,
|
|
564
|
+
failingCommitCount: detailTargets.length,
|
|
565
|
+
rateLimitCost,
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* ONE-TIME deepening of the trunk window: walk `history(since: now − TREND_DAYS)` page by page
|
|
570
|
+
* and upsert every commit, so the branch-trends DayStrip spans its whole 90-day read window
|
|
571
|
+
* instead of only the newest BRANCH_COMMIT_WINDOW commits the live snapshot retains. Runs after
|
|
572
|
+
* a repo's first full sync (and a forced deep re-sync) — see sync-manager.ts.
|
|
573
|
+
*
|
|
574
|
+
* Deliberate differences from the live snapshot, all cost-driven:
|
|
575
|
+
* • NO phase-2 failing-check detail — the trend cells need only `ciStatus`, and per the
|
|
576
|
+
* partial-response policy an absent detail simply omits the column (`failingChecksToWrite`
|
|
577
|
+
* returns undefined for a non-green commit with no phase-2 entry), so any detail the live
|
|
578
|
+
* window already stored is left untouched where the walks overlap.
|
|
579
|
+
* • NO `repos` head-column write and NO trim — the live snapshot owns both; every row this
|
|
580
|
+
* walk inserts sits inside the TREND_DAYS window the hybrid trim retains.
|
|
581
|
+
* • The first page re-reads the live window from the branch tip rather than resuming a stored
|
|
582
|
+
* cursor — history cursors are position-relative and trunk moves during the PR walk, so a
|
|
583
|
+
* handed-off cursor could silently skip commits; one duplicate page is 4 points, and the
|
|
584
|
+
* upsert is idempotent.
|
|
585
|
+
*
|
|
586
|
+
* Old commits' rollups are FINAL for our purposes (a historical re-run that flips one is rarer
|
|
587
|
+
* than the live window's own retro-flips, which docs/MERGE-CI-TRUNK.md already documents), so
|
|
588
|
+
* never re-observing these rows is by design. A commit whose CI never finished stays `pending`
|
|
589
|
+
* and counts as neither pass nor fail in the trends read.
|
|
590
|
+
*/
|
|
591
|
+
export async function backfillBranchHistory(opts) {
|
|
592
|
+
const { owner, name, repoId, accountId, token } = opts;
|
|
593
|
+
const client = getGraphqlClientFor(token);
|
|
594
|
+
const fullName = `${owner}/${name}`;
|
|
595
|
+
const since = new Date(Date.now() - TREND_DAYS * DAY_MS).toISOString();
|
|
596
|
+
let after = null;
|
|
597
|
+
let pages = 0;
|
|
598
|
+
let commitCount = 0;
|
|
599
|
+
let rateLimitCost = 0;
|
|
600
|
+
while (pages < BRANCH_BACKFILL_MAX_PAGES) {
|
|
601
|
+
if (opts.shouldCancel?.())
|
|
602
|
+
break;
|
|
603
|
+
// Shares its token with the account's PR walks: when the budget is low or a limit was
|
|
604
|
+
// observed, wait (cancellably) before spending ~4 points on another history page. A
|
|
605
|
+
// cancel mid-wait breaks like any cancel — the walk stays strictly non-fatal and the
|
|
606
|
+
// result is disclosed as truncated.
|
|
607
|
+
if ((await gateBudget(accountId, { shouldCancel: opts.shouldCancel })) === 'cancelled') {
|
|
608
|
+
break;
|
|
609
|
+
}
|
|
610
|
+
const resp = await withGithubRetry(() => graphqlTolerant(client, DEFAULT_BRANCH_HISTORY_QUERY, { owner, name, first: BRANCH_COMMIT_WINDOW, since, after }, (errors) => opts.log?.warn(`branch-backfill ${fullName}: partial GraphQL — continuing without forbidden fields`, errors)));
|
|
611
|
+
pages += 1;
|
|
612
|
+
rateLimitCost += resp.rateLimit?.cost ?? 0;
|
|
613
|
+
const ref = resp.repository?.defaultBranchRef ?? null;
|
|
614
|
+
const history = ref?.target?.history ?? null;
|
|
615
|
+
const nodes = (history?.nodes ?? []).filter((n) => n != null && typeof n.oid === 'string');
|
|
616
|
+
if (nodes.length > 0) {
|
|
617
|
+
const userIdByLogin = await resolveKnownLogins(nodes);
|
|
618
|
+
const branchName = ref?.name ?? null;
|
|
619
|
+
await runTransaction(async (tx) => {
|
|
620
|
+
await upsertBranchCommitRows(tx, { accountId, repoId, fullName, branchName }, nodes, userIdByLogin, new Map());
|
|
621
|
+
});
|
|
622
|
+
commitCount += nodes.length;
|
|
623
|
+
}
|
|
624
|
+
// An absent pageInfo is "not received" (tolerant partial), which must stop the walk rather
|
|
625
|
+
// than throw or spin; endCursor null with hasNextPage true is treated the same way.
|
|
626
|
+
const pageInfo = history?.pageInfo ?? null;
|
|
627
|
+
if (pageInfo?.hasNextPage !== true || pageInfo.endCursor == null) {
|
|
628
|
+
return { pages, commitCount, rateLimitCost, truncated: false };
|
|
629
|
+
}
|
|
630
|
+
after = pageInfo.endCursor;
|
|
631
|
+
}
|
|
632
|
+
return { pages, commitCount, rateLimitCost, truncated: true };
|
|
633
|
+
}
|
|
634
|
+
//# sourceMappingURL=branch-status.js.map
|