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,454 @@
|
|
|
1
|
+
import { graphql, GraphqlResponseError } from '@octokit/graphql';
|
|
2
|
+
import { getGithubToken } from './auth.js';
|
|
3
|
+
// True when the partial errors are GitHub's SAML-SSO wall: the token authenticates but isn't
|
|
4
|
+
// authorized for the repo owner's org, so the whole `repository` node is forbidden. GitHub's
|
|
5
|
+
// message is "Resource protected by organization SAML enforcement. You must grant your OAuth
|
|
6
|
+
// token access to this organization." This gates PRs/checks even on PUBLIC repos of a SAML org.
|
|
7
|
+
export function isSamlBlock(errors) {
|
|
8
|
+
return (Array.isArray(errors) &&
|
|
9
|
+
errors.some((e) => {
|
|
10
|
+
// GraphQL exposes a machine-readable flag on the error — the most reliable signal
|
|
11
|
+
// (docs: FORBIDDEN + extensions.saml_failure === true). Fall back to the message text.
|
|
12
|
+
if (e.extensions?.saml_failure === true)
|
|
13
|
+
return true;
|
|
14
|
+
return /saml enforcement|grant your oauth token access/i.test(e.message ?? '');
|
|
15
|
+
}));
|
|
16
|
+
}
|
|
17
|
+
// A parenthetical hint appended to a partial-GraphQL log. SAML enforcement gets the actionable
|
|
18
|
+
// re-auth hint; otherwise, ONLY when the forbidden field is a CI-checks field (so a NOT_FOUND —
|
|
19
|
+
// deleted/inaccessible PR — isn't mislabelled). Empty otherwise (the raw errors still get logged).
|
|
20
|
+
export function graphqlChecksHint(errors) {
|
|
21
|
+
if (isSamlBlock(errors)) {
|
|
22
|
+
return " — the sign-in token isn't authorized for this org's SAML SSO; re-authorize it inside an active SAML session for the org (see docs/GITHUB-AUTH-SETUP.md)";
|
|
23
|
+
}
|
|
24
|
+
const mentionsChecks = Array.isArray(errors) &&
|
|
25
|
+
errors.some((e) => {
|
|
26
|
+
const path = e.path;
|
|
27
|
+
const inPath = Array.isArray(path) && path.some((p) => /statuscheckrollup|check/i.test(String(p)));
|
|
28
|
+
const inMsg = /check\b|statuscheckrollup/i.test(e.message ?? '');
|
|
29
|
+
return inPath || inMsg;
|
|
30
|
+
});
|
|
31
|
+
return mentionsChecks
|
|
32
|
+
? " — the sign-in token can't read CI checks here (GitHub App: install on this org with Checks read; OAuth App: re-authorize; or it's a private repo)"
|
|
33
|
+
: '';
|
|
34
|
+
}
|
|
35
|
+
// Compact one-line summary of a GraphQL `errors` array for logging.
|
|
36
|
+
export function summarizeGraphqlErrors(errors) {
|
|
37
|
+
if (!Array.isArray(errors))
|
|
38
|
+
return String(errors);
|
|
39
|
+
return errors
|
|
40
|
+
.slice(0, 5)
|
|
41
|
+
.map((e) => {
|
|
42
|
+
const type = e.type ?? 'ERROR';
|
|
43
|
+
const path = e.path?.join('.') ?? '?';
|
|
44
|
+
const message = e.message ?? '';
|
|
45
|
+
return `${type}@${path}: ${message}`;
|
|
46
|
+
})
|
|
47
|
+
.join(' | ');
|
|
48
|
+
}
|
|
49
|
+
// Run a GraphQL query, TOLERATING GitHub's *partial* errors. GitHub answers HTTP 200 with a
|
|
50
|
+
// non-empty `errors` array AND partial `data` when the token may read most of a query but is
|
|
51
|
+
// FORBIDDEN one sub-field — e.g. CI check runs (`statusCheckRollup.contexts`) on a private repo
|
|
52
|
+
// the token can't reach, or on a token minted before it was granted the scope to read them. By
|
|
53
|
+
// default `@octokit/graphql` THROWS on any such response, throwing away the usable partial data
|
|
54
|
+
// — so one un-permitted field would wipe the PR body/comments/reviewers. This returns the
|
|
55
|
+
// partial data instead (the forbidden fields arrive as null) and reports the errors via
|
|
56
|
+
// `onPartial`. A response with NO usable data (auth failure, rate limit, network) still throws.
|
|
57
|
+
export async function graphqlTolerant(client, query, variables, onPartial) {
|
|
58
|
+
try {
|
|
59
|
+
return await client(query, variables);
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
if (err instanceof GraphqlResponseError && err.data != null) {
|
|
63
|
+
onPartial?.(err.errors);
|
|
64
|
+
return err.data;
|
|
65
|
+
}
|
|
66
|
+
throw err;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// True for GitHub failures worth RETRYING: transient upstream/gateway faults (a 5xx
|
|
70
|
+
// from GitHub's edge — the fat activity query on a huge repo routinely 502s at nginx)
|
|
71
|
+
// and low-level network faults (reset/timeout/DNS). Deliberately NOT ordinary 4xx
|
|
72
|
+
// (auth / not-found / rate-limit-with-reset) nor GraphQL partial-data errors — those
|
|
73
|
+
// are handled elsewhere (graphqlTolerant) and must not be silently re-attempted. A 502
|
|
74
|
+
// arrives as an HTML error page (not GraphQL JSON), so match BOTH a structured status
|
|
75
|
+
// (octokit RequestError `.status`) AND the message text as a fallback.
|
|
76
|
+
export function isRetryableGithubError(err) {
|
|
77
|
+
const e = err;
|
|
78
|
+
const status = e.status ?? e.response?.status;
|
|
79
|
+
if (status === 500 || status === 502 || status === 503 || status === 504)
|
|
80
|
+
return true;
|
|
81
|
+
if (/^(ECONNRESET|ETIMEDOUT|EAI_AGAIN|ENOTFOUND|ECONNREFUSED|EPIPE)$/.test(e.code ?? '')) {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
return /\b(50[0234])\b|bad gateway|gateway time-?out|service unavailable|temporarily unavailable|socket hang up|fetch failed|econnreset|etimedout/i.test(e.message ?? '');
|
|
85
|
+
}
|
|
86
|
+
// Classify a GitHub failure as RATE-LIMITED (primary GraphQL/REST exhaustion, or the
|
|
87
|
+
// secondary "abuse" limiter) and extract a resume time when the response carried one.
|
|
88
|
+
// Deliberately a SEPARATE classifier from isRetryableGithubError: retryable means "try
|
|
89
|
+
// again in milliseconds", limited means "pause until the window resets" — the sync's
|
|
90
|
+
// budget gate (github/rate-budget.ts + sync-repo.ts) owns that wait, and widening the
|
|
91
|
+
// retry predicate would instead hammer a limited token.
|
|
92
|
+
//
|
|
93
|
+
// Shapes recognised:
|
|
94
|
+
// • GraphQL: `errors[].type === 'RATE_LIMITED'` (GitHub answers HTTP 200, data null —
|
|
95
|
+
// which is why graphqlTolerant RETHROWS it), or a rate-limit message with no usable
|
|
96
|
+
// data. GraphqlResponseError exposes the response headers, so a reset time rides in.
|
|
97
|
+
// • REST: status 429 (Too Many Requests IS the limiter, by definition), or a 403 whose
|
|
98
|
+
// message carries GitHub's rate-limit/abuse wording (a plain 403 forbidden is NOT
|
|
99
|
+
// limited). ghRest attaches `status` + the rate-limit headers onto its thrown Error
|
|
100
|
+
// so this works without re-fetching anything.
|
|
101
|
+
//
|
|
102
|
+
// resumeAt: `retry-after` (relative seconds — the secondary limiter's header) wins, else
|
|
103
|
+
// `x-ratelimit-reset` (epoch seconds), else null (the caller falls back to a default).
|
|
104
|
+
export function isRateLimitError(err) {
|
|
105
|
+
const e = err;
|
|
106
|
+
const header = (name) => {
|
|
107
|
+
const h = e.headers?.[name] ?? e.response?.headers?.[name];
|
|
108
|
+
if (typeof h === 'string' && h.length > 0)
|
|
109
|
+
return h;
|
|
110
|
+
if (typeof h === 'number')
|
|
111
|
+
return String(h);
|
|
112
|
+
return null;
|
|
113
|
+
};
|
|
114
|
+
const resumeAt = () => {
|
|
115
|
+
const retryAfter = header('retry-after');
|
|
116
|
+
if (retryAfter != null) {
|
|
117
|
+
const secs = Number.parseInt(retryAfter, 10);
|
|
118
|
+
if (Number.isFinite(secs) && secs >= 0)
|
|
119
|
+
return new Date(Date.now() + secs * 1000);
|
|
120
|
+
}
|
|
121
|
+
const reset = header('x-ratelimit-reset');
|
|
122
|
+
if (reset != null) {
|
|
123
|
+
const epoch = Number.parseInt(reset, 10);
|
|
124
|
+
if (Number.isFinite(epoch) && epoch > 0)
|
|
125
|
+
return new Date(epoch * 1000);
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
};
|
|
129
|
+
if (Array.isArray(e.errors) &&
|
|
130
|
+
e.errors.some((er) => er.type === 'RATE_LIMITED')) {
|
|
131
|
+
return { limited: true, resumeAt: resumeAt() };
|
|
132
|
+
}
|
|
133
|
+
const status = e.status ?? e.response?.status;
|
|
134
|
+
if (status === 429)
|
|
135
|
+
return { limited: true, resumeAt: resumeAt() };
|
|
136
|
+
if (status === 403 && /rate limit|abuse/i.test(e.message ?? '')) {
|
|
137
|
+
return { limited: true, resumeAt: resumeAt() };
|
|
138
|
+
}
|
|
139
|
+
// Message-only fallback (a GraphQL transport variant with neither structured errors nor
|
|
140
|
+
// a status). Guarded on "no usable data" + "no status" so a 4xx/5xx that merely MENTIONS
|
|
141
|
+
// rate limits can't sneak in.
|
|
142
|
+
if (status == null &&
|
|
143
|
+
e.errors == null &&
|
|
144
|
+
e.data == null &&
|
|
145
|
+
/rate limit/i.test(e.message ?? '')) {
|
|
146
|
+
return { limited: true, resumeAt: resumeAt() };
|
|
147
|
+
}
|
|
148
|
+
return { limited: false, resumeAt: null };
|
|
149
|
+
}
|
|
150
|
+
// Run a GitHub call with bounded exponential-backoff retries on TRANSIENT failures
|
|
151
|
+
// (see isRetryableGithubError). A single 502 on any page must not abort a multi-page
|
|
152
|
+
// backfill of a large repo — without this, adding a big repo (every curated suggestion
|
|
153
|
+
// is one) frequently loads only a partial window before the walk dies. Non-retryable
|
|
154
|
+
// errors (4xx, GraphQL partial) propagate immediately. Backoff: 500ms, 1s, 2s.
|
|
155
|
+
export async function withGithubRetry(fn, opts = {}) {
|
|
156
|
+
const retries = opts.retries ?? 3;
|
|
157
|
+
const base = opts.baseDelayMs ?? 500;
|
|
158
|
+
for (let attempt = 1;; attempt++) {
|
|
159
|
+
try {
|
|
160
|
+
return await fn();
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
if (attempt > retries || !isRetryableGithubError(err))
|
|
164
|
+
throw err;
|
|
165
|
+
const delayMs = base * 2 ** (attempt - 1);
|
|
166
|
+
opts.onRetry?.(attempt, delayMs, err);
|
|
167
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// Per-account GitHub clients. There is deliberately NO module-level token /
|
|
172
|
+
// client cache: in a multi-tenant (cloud) process a cached token would leak one
|
|
173
|
+
// account's credentials into another account's request. The token is ALWAYS
|
|
174
|
+
// passed in by the caller (resolved from the owning account), so isolation is
|
|
175
|
+
// structural. See auth/account.ts#getAccessToken for where the token comes from.
|
|
176
|
+
export function getGraphqlClientFor(token) {
|
|
177
|
+
return graphql.defaults({ headers: { authorization: `token ${token}` } });
|
|
178
|
+
}
|
|
179
|
+
async function ghRest(token, method, path, body) {
|
|
180
|
+
const res = await fetch(`https://api.github.com${path}`, {
|
|
181
|
+
method,
|
|
182
|
+
headers: {
|
|
183
|
+
authorization: `token ${token}`,
|
|
184
|
+
accept: 'application/vnd.github+json',
|
|
185
|
+
'x-github-api-version': '2022-11-28',
|
|
186
|
+
...(body !== undefined ? { 'content-type': 'application/json' } : {}),
|
|
187
|
+
},
|
|
188
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
189
|
+
});
|
|
190
|
+
if (!res.ok) {
|
|
191
|
+
const text = await res.text().catch(() => '');
|
|
192
|
+
const err = new Error(`GitHub REST ${method} ${path} -> ${res.status}: ${text.slice(0, 300)}`);
|
|
193
|
+
// Structured status + the rate-limit headers, so isRateLimitError can classify a REST
|
|
194
|
+
// 403/429 and derive a resume time without message archaeology. Nulls kept as-is —
|
|
195
|
+
// the classifier's header lookup only honours strings.
|
|
196
|
+
Object.assign(err, {
|
|
197
|
+
status: res.status,
|
|
198
|
+
headers: {
|
|
199
|
+
'retry-after': res.headers.get('retry-after'),
|
|
200
|
+
'x-ratelimit-remaining': res.headers.get('x-ratelimit-remaining'),
|
|
201
|
+
'x-ratelimit-reset': res.headers.get('x-ratelimit-reset'),
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
throw err;
|
|
205
|
+
}
|
|
206
|
+
return res.json();
|
|
207
|
+
}
|
|
208
|
+
// REST GET (per-commit changed-files etc.) for a specific account's token.
|
|
209
|
+
export function ghRestGetFor(token, path) {
|
|
210
|
+
return ghRest(token, 'GET', path);
|
|
211
|
+
}
|
|
212
|
+
// Conditional REST GET for the adaptive-sync change probe (Phase 2 — see
|
|
213
|
+
// docs/REALTIME-SYNC.md). Sends `If-None-Match: <etag>` when a prior ETag is known and
|
|
214
|
+
// returns the status + fresh ETag WITHOUT throwing — a 304 (Not Modified) means nothing
|
|
215
|
+
// changed and, crucially, does NOT count against GitHub's primary rate limit. The body is
|
|
216
|
+
// drained and discarded (we only care about changed-or-not + the new ETag). A non-2xx /
|
|
217
|
+
// non-304 (403/404/network) is reported as `notModified: false` so the caller falls back
|
|
218
|
+
// to a full walk rather than silently skipping.
|
|
219
|
+
export async function ghRestGetConditional(token, path, etag) {
|
|
220
|
+
const res = await fetch(`https://api.github.com${path}`, {
|
|
221
|
+
method: 'GET',
|
|
222
|
+
headers: {
|
|
223
|
+
authorization: `token ${token}`,
|
|
224
|
+
accept: 'application/vnd.github+json',
|
|
225
|
+
'x-github-api-version': '2022-11-28',
|
|
226
|
+
...(etag ? { 'if-none-match': etag } : {}),
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
// Drain the body so the socket is freed (we never read it).
|
|
230
|
+
await res.arrayBuffer().catch(() => { });
|
|
231
|
+
return {
|
|
232
|
+
status: res.status,
|
|
233
|
+
notModified: res.status === 304,
|
|
234
|
+
etag: res.headers.get('etag'),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
// REST GET returning the raw response TEXT (not JSON), for endpoints with a plain-text
|
|
238
|
+
// body — notably the Actions job-logs endpoint, which 302-redirects to a signed
|
|
239
|
+
// download URL. fetch follows the redirect automatically AND strips the Authorization
|
|
240
|
+
// header on the cross-origin hop, so our token is never sent to the signed URL. Does
|
|
241
|
+
// NOT throw on a non-2xx status — returns it so the caller can degrade gracefully
|
|
242
|
+
// (logs expire after ~90 days / on a re-run → 404/410; missing actions:read → 403).
|
|
243
|
+
export async function ghRestGetText(token, path) {
|
|
244
|
+
const res = await fetch(`https://api.github.com${path}`, {
|
|
245
|
+
method: 'GET',
|
|
246
|
+
headers: {
|
|
247
|
+
authorization: `token ${token}`,
|
|
248
|
+
'x-github-api-version': '2022-11-28',
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
const text = await res.text().catch(() => '');
|
|
252
|
+
return { status: res.status, ok: res.ok, text };
|
|
253
|
+
}
|
|
254
|
+
// ADDITIVE (Actions job logs, byte-window viewer): REST GET returning the RAW Response
|
|
255
|
+
// WITHOUT reading the body and WITHOUT following redirects (`redirect: 'manual'`), so
|
|
256
|
+
// the caller can:
|
|
257
|
+
// (a) read the `location` of the Actions job-logs 302 and issue its OWN byte-`Range`
|
|
258
|
+
// request against the short-lived signed blob URL (which does honour Range), and
|
|
259
|
+
// (b) STREAM + cap a potentially multi-MB body instead of buffering the whole thing.
|
|
260
|
+
// The signed URL must NEVER be handed to a client — it is unauthenticated and would
|
|
261
|
+
// bypass our per-account ownership check. Nothing here throws on a non-2xx; the caller
|
|
262
|
+
// branches on `res.status`. The Authorization header is only ever sent to api.github.com
|
|
263
|
+
// because we do not follow the cross-origin hop ourselves.
|
|
264
|
+
export async function ghRestGetRaw(token, path, init) {
|
|
265
|
+
return fetch(`https://api.github.com${path}`, {
|
|
266
|
+
method: 'GET',
|
|
267
|
+
redirect: 'manual',
|
|
268
|
+
headers: {
|
|
269
|
+
authorization: `token ${token}`,
|
|
270
|
+
'x-github-api-version': '2022-11-28',
|
|
271
|
+
},
|
|
272
|
+
signal: init?.signal,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
// REST GET a repo file's RAW contents (Accept: application/vnd.github.raw), NOT throwing
|
|
276
|
+
// on a non-2xx — returns the status + body so the caller can branch. Used to fetch a
|
|
277
|
+
// repo's CODEOWNERS file (404 when absent → degrade to no CODEOWNERS suggestions) and the
|
|
278
|
+
// advisor's bot-config reads. The raw media type returns the file bytes directly (no
|
|
279
|
+
// base64/JSON envelope to decode). `ref` (optional) reads at a specific branch/sha —
|
|
280
|
+
// omitted, GitHub serves the default branch, which is the config that governs FUTURE
|
|
281
|
+
// reviews (the advisor's read point).
|
|
282
|
+
export async function ghRestGetContentRaw(token, path, ref) {
|
|
283
|
+
const url = ref
|
|
284
|
+
? `https://api.github.com${path}?ref=${encodeURIComponent(ref)}`
|
|
285
|
+
: `https://api.github.com${path}`;
|
|
286
|
+
const res = await fetch(url, {
|
|
287
|
+
method: 'GET',
|
|
288
|
+
headers: {
|
|
289
|
+
authorization: `token ${token}`,
|
|
290
|
+
accept: 'application/vnd.github.raw',
|
|
291
|
+
'x-github-api-version': '2022-11-28',
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
const text = await res.text().catch(() => '');
|
|
295
|
+
return { status: res.status, ok: res.ok, text };
|
|
296
|
+
}
|
|
297
|
+
// REST GET one directory's contents listing (the JSON contents API — for a FILE it returns
|
|
298
|
+
// an object, for a directory an array). Status-returning like ghRestGetContentRaw: a 404 is
|
|
299
|
+
// the ordinary "that directory doesn't exist" outcome (e.g. a repo with no
|
|
300
|
+
// .github/workflows/). Entries are name/path/type/size only — file BYTES go through
|
|
301
|
+
// ghRestGetContentRaw so there is exactly one place decoding repo-authored content.
|
|
302
|
+
export async function ghRestGetContentDir(token, path, ref) {
|
|
303
|
+
const url = ref
|
|
304
|
+
? `https://api.github.com${path}?ref=${encodeURIComponent(ref)}`
|
|
305
|
+
: `https://api.github.com${path}`;
|
|
306
|
+
const res = await fetch(url, {
|
|
307
|
+
method: 'GET',
|
|
308
|
+
headers: {
|
|
309
|
+
authorization: `token ${token}`,
|
|
310
|
+
accept: 'application/vnd.github+json',
|
|
311
|
+
'x-github-api-version': '2022-11-28',
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
if (!res.ok) {
|
|
315
|
+
await res.text().catch(() => '');
|
|
316
|
+
return { status: res.status, ok: false, entries: [] };
|
|
317
|
+
}
|
|
318
|
+
const body = await res.json().catch(() => null);
|
|
319
|
+
if (!Array.isArray(body)) {
|
|
320
|
+
// A file path (object body) or an unreadable payload — not a directory listing.
|
|
321
|
+
return { status: res.status, ok: false, entries: [] };
|
|
322
|
+
}
|
|
323
|
+
const entries = body
|
|
324
|
+
.map((e) => {
|
|
325
|
+
const it = e;
|
|
326
|
+
if (typeof it.name !== 'string' || typeof it.path !== 'string')
|
|
327
|
+
return null;
|
|
328
|
+
const type = it.type === 'dir' ? 'dir' : it.type === 'file' ? 'file' : null;
|
|
329
|
+
if (!type)
|
|
330
|
+
return null;
|
|
331
|
+
return {
|
|
332
|
+
name: it.name,
|
|
333
|
+
path: it.path,
|
|
334
|
+
type,
|
|
335
|
+
size: typeof it.size === 'number' ? it.size : 0,
|
|
336
|
+
};
|
|
337
|
+
})
|
|
338
|
+
.filter((e) => Boolean(e));
|
|
339
|
+
return { status: res.status, ok: true, entries };
|
|
340
|
+
}
|
|
341
|
+
// REST GET in GitHub's raw `diff` media type (Accept: application/vnd.github.diff),
|
|
342
|
+
// NOT throwing on a non-2xx — returns the status + body so the caller can branch. GitHub
|
|
343
|
+
// caps this media type at 20,000 lines and 406s past it (a huge PR), which the caller
|
|
344
|
+
// handles by falling back to the per-file endpoint. Mirrors ghRestGetText's non-throwing
|
|
345
|
+
// contract.
|
|
346
|
+
export async function ghRestGetDiffStatus(token, path) {
|
|
347
|
+
const res = await fetch(`https://api.github.com${path}`, {
|
|
348
|
+
method: 'GET',
|
|
349
|
+
headers: {
|
|
350
|
+
authorization: `token ${token}`,
|
|
351
|
+
accept: 'application/vnd.github.diff',
|
|
352
|
+
'x-github-api-version': '2022-11-28',
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
const text = await res.text().catch(() => '');
|
|
356
|
+
return { status: res.status, ok: res.ok, text };
|
|
357
|
+
}
|
|
358
|
+
// Throwing variant of the above — the per-account, cloud-ready way to fetch a PR's
|
|
359
|
+
// unified diff (vs the local-only `gh pr diff`). Throws on a non-2xx status.
|
|
360
|
+
export async function ghRestGetDiff(token, path) {
|
|
361
|
+
const res = await ghRestGetDiffStatus(token, path);
|
|
362
|
+
if (!res.ok) {
|
|
363
|
+
throw new Error(`GitHub REST GET(diff) ${path} -> ${res.status}: ${res.text.slice(0, 300)}`);
|
|
364
|
+
}
|
|
365
|
+
return res.text;
|
|
366
|
+
}
|
|
367
|
+
// REST PUT returning the parsed STATUS + body WITHOUT throwing on a non-2xx, so the caller can
|
|
368
|
+
// map GitHub's meaningful merge / update-branch statuses to structured results (405 not
|
|
369
|
+
// mergeable, 409 head-sha mismatch, 422 method disallowed / can't update). Per-account token.
|
|
370
|
+
export async function ghRestPutStatus(token, path, body) {
|
|
371
|
+
const res = await fetch(`https://api.github.com${path}`, {
|
|
372
|
+
method: 'PUT',
|
|
373
|
+
headers: {
|
|
374
|
+
authorization: `token ${token}`,
|
|
375
|
+
accept: 'application/vnd.github+json',
|
|
376
|
+
'x-github-api-version': '2022-11-28',
|
|
377
|
+
...(body !== undefined ? { 'content-type': 'application/json' } : {}),
|
|
378
|
+
},
|
|
379
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
380
|
+
});
|
|
381
|
+
const text = await res.text().catch(() => '');
|
|
382
|
+
let json = null;
|
|
383
|
+
try {
|
|
384
|
+
json = text ? JSON.parse(text) : null;
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
/* non-JSON body (rare on these endpoints) */
|
|
388
|
+
}
|
|
389
|
+
return { status: res.status, ok: res.ok, json, text };
|
|
390
|
+
}
|
|
391
|
+
// REST PATCH returning status (updating a pull request — e.g. closing it via
|
|
392
|
+
// `{ state: 'closed' }`). Mirrors ghRestPutStatus: never throws on a non-2xx, returns the
|
|
393
|
+
// status + parsed body so the caller can map GitHub's error to an HTTP response.
|
|
394
|
+
export async function ghRestPatchStatus(token, path, body) {
|
|
395
|
+
const res = await fetch(`https://api.github.com${path}`, {
|
|
396
|
+
method: 'PATCH',
|
|
397
|
+
headers: {
|
|
398
|
+
authorization: `token ${token}`,
|
|
399
|
+
accept: 'application/vnd.github+json',
|
|
400
|
+
'x-github-api-version': '2022-11-28',
|
|
401
|
+
...(body !== undefined ? { 'content-type': 'application/json' } : {}),
|
|
402
|
+
},
|
|
403
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
404
|
+
});
|
|
405
|
+
const text = await res.text().catch(() => '');
|
|
406
|
+
let json = null;
|
|
407
|
+
try {
|
|
408
|
+
json = text ? JSON.parse(text) : null;
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
/* non-JSON body (rare on these endpoints) */
|
|
412
|
+
}
|
|
413
|
+
return { status: res.status, ok: res.ok, json, text };
|
|
414
|
+
}
|
|
415
|
+
// REST POST (submitting a PR review — inline line comments require the REST
|
|
416
|
+
// reviews endpoint) for a specific account's token.
|
|
417
|
+
export function ghRestPostFor(token, path, body) {
|
|
418
|
+
return ghRest(token, 'POST', path, body);
|
|
419
|
+
}
|
|
420
|
+
// REST POST to an endpoint that returns NO body (201/204 No Content) — e.g. the Actions
|
|
421
|
+
// "rerun" / "rerun-failed-jobs" endpoints. Same auth + fail-loud-on-non-2xx as ghRest,
|
|
422
|
+
// but does NOT call res.json() (which would throw `Unexpected end of JSON input` on the
|
|
423
|
+
// empty success body). Returns nothing.
|
|
424
|
+
export async function ghRestPostNoContent(token, path, body) {
|
|
425
|
+
const res = await fetch(`https://api.github.com${path}`, {
|
|
426
|
+
method: 'POST',
|
|
427
|
+
headers: {
|
|
428
|
+
authorization: `token ${token}`,
|
|
429
|
+
accept: 'application/vnd.github+json',
|
|
430
|
+
'x-github-api-version': '2022-11-28',
|
|
431
|
+
...(body !== undefined ? { 'content-type': 'application/json' } : {}),
|
|
432
|
+
},
|
|
433
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
434
|
+
});
|
|
435
|
+
if (!res.ok) {
|
|
436
|
+
const text = await res.text().catch(() => '');
|
|
437
|
+
throw new Error(`GitHub REST POST ${path} -> ${res.status}: ${text.slice(0, 300)}`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
// ---- Local-only convenience wrappers ----
|
|
441
|
+
// Use the gh CLI token. ONLY for code paths that run in local mode — the Claude
|
|
442
|
+
// Review posting path (post-review.ts) and clone-manager.ts, both force-disabled
|
|
443
|
+
// in cloud. Caching the gh token is safe because local mode is single-account.
|
|
444
|
+
let localToken = null;
|
|
445
|
+
function localGhToken() {
|
|
446
|
+
return (localToken ??= getGithubToken());
|
|
447
|
+
}
|
|
448
|
+
export function ghRestGet(path) {
|
|
449
|
+
return ghRestGetFor(localGhToken(), path);
|
|
450
|
+
}
|
|
451
|
+
export function ghRestPost(path, body) {
|
|
452
|
+
return ghRestPostFor(localGhToken(), path, body);
|
|
453
|
+
}
|
|
454
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// CODEOWNERS-based reviewer suggestions (CORE). Fetches a repo's CODEOWNERS file via the
|
|
2
|
+
// contents API, parses it, and — given a PR's changed paths — returns the owning users +
|
|
3
|
+
// teams. Best-effort throughout: any failure (no file, token can't read the repo, a parse
|
|
4
|
+
// hiccup) degrades to "no CODEOWNERS suggestions", NEVER an error on the PR-detail path.
|
|
5
|
+
//
|
|
6
|
+
// Why CODEOWNERS and not the org/teams API: the contents API is reachable in every auth
|
|
7
|
+
// mode (local gh token, cloud OAuth `public_repo`, GitHub App with contents:read), and a
|
|
8
|
+
// `@org/team` owner is directly requestable as a review via `team_reviewers` — no need to
|
|
9
|
+
// expand team membership (which needs `read:org` + org membership and is unreliable under
|
|
10
|
+
// the cloud GitHub App). Individual `@user` owners become user suggestions directly.
|
|
11
|
+
import { ghRestGetContentRaw } from './client.js';
|
|
12
|
+
// The candidate file locations GitHub honours, in the order it resolves them.
|
|
13
|
+
const CODEOWNERS_PATHS = ['.github/CODEOWNERS', 'CODEOWNERS', 'docs/CODEOWNERS'];
|
|
14
|
+
// Per-(account, repo) cache. CODEOWNERS changes rarely, so a short TTL keeps the extra
|
|
15
|
+
// contents-API call off the hot path while staying reasonably fresh. A `null` rules value
|
|
16
|
+
// is cached too (the common "repo has no CODEOWNERS" case) so we don't refetch a 404 on
|
|
17
|
+
// every PR open. Keyed by account so one tenant's token result never leaks to another.
|
|
18
|
+
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
19
|
+
const cache = new Map();
|
|
20
|
+
// ---- Hard limits on attacker-supplied input ----
|
|
21
|
+
// A CODEOWNERS file is fetched from a repository, which means its contents are chosen by
|
|
22
|
+
// whoever controls that repository — in cloud, anyone who can sign up and add their own
|
|
23
|
+
// repo. Every one of these rules is then regex-matched against every changed path of every
|
|
24
|
+
// PR on the request thread of a SINGLE-threaded server, so unbounded input here is a
|
|
25
|
+
// denial-of-service on the whole deployment, not just on the caller.
|
|
26
|
+
//
|
|
27
|
+
// A real CODEOWNERS file is a few dozen lines; GitHub itself stops honouring one past 3 MB
|
|
28
|
+
// and only applies the first 1000 rules. These caps sit comfortably above any legitimate
|
|
29
|
+
// file and turn a hostile one into a truncated, harmless one.
|
|
30
|
+
const MAX_CODEOWNERS_BYTES = 256 * 1024;
|
|
31
|
+
const MAX_RULES = 500;
|
|
32
|
+
const MAX_PATTERN_LENGTH = 512;
|
|
33
|
+
/** Cap the paths matched per PR. A PR touching more files than this is already an outlier,
|
|
34
|
+
* and suggestions from the first N are as useful as suggestions from all of them. */
|
|
35
|
+
const MAX_PATHS_MATCHED = 300;
|
|
36
|
+
// Convert a CODEOWNERS (gitignore-style) path pattern into a regex over POSIX file paths.
|
|
37
|
+
// A best-effort subset matching GitHub's documented CODEOWNERS semantics: `*` matches
|
|
38
|
+
// within a single segment (NOT across `/`), `**` across segments, `?` one non-slash char;
|
|
39
|
+
// a leading `/` or any internal `/` anchors to the repo root, otherwise the pattern may
|
|
40
|
+
// match at any directory depth; a trailing `/` (or a bare directory name) owns everything
|
|
41
|
+
// under that directory. Exported for the unit test that locks these semantics.
|
|
42
|
+
export function globToRegExp(raw) {
|
|
43
|
+
let p = raw.trim();
|
|
44
|
+
if (p.length > MAX_PATTERN_LENGTH) {
|
|
45
|
+
throw new Error('codeowners pattern too long');
|
|
46
|
+
}
|
|
47
|
+
// ---- ReDoS guard: collapse repeated globs BEFORE compiling ----
|
|
48
|
+
// Each segment-aligned `**/` compiles to `(?:.*/)?` below. Two of those in a row give the
|
|
49
|
+
// regex engine two nullable, overlapping ways to consume the same directory prefix, and
|
|
50
|
+
// N in a row make that 2^N — so `**/**/**/…/zzz.txt` against a deep path backtracks
|
|
51
|
+
// effectively forever, freezing the event loop for every tenant.
|
|
52
|
+
//
|
|
53
|
+
// Collapsing is not merely a mitigation, it is SEMANTICALLY EXACT: "any number of leading
|
|
54
|
+
// directories, then any number of leading directories" is the same language as "any
|
|
55
|
+
// number of leading directories". Same for a run of bare `**`, and for `**` adjacent to
|
|
56
|
+
// `*`. After this, at most one `(?:.*/)?` can come from each distinct run, so the compiled
|
|
57
|
+
// pattern is linear to match.
|
|
58
|
+
p = p
|
|
59
|
+
// `**/**/` … → `**/`
|
|
60
|
+
.replace(/(?:\*\*\/)+/g, '**/')
|
|
61
|
+
// `****…` (a run of 3+ stars, or `**` next to `*`) → `**`
|
|
62
|
+
.replace(/\*{3,}/g, '**')
|
|
63
|
+
// `**/**` (trailing, not slash-terminated) → `**`
|
|
64
|
+
.replace(/\*\*(?:\/\*\*)+/g, '**');
|
|
65
|
+
const dirOnly = p.endsWith('/');
|
|
66
|
+
if (dirOnly)
|
|
67
|
+
p = p.slice(0, -1);
|
|
68
|
+
const startSlash = p.startsWith('/');
|
|
69
|
+
if (startSlash)
|
|
70
|
+
p = p.slice(1);
|
|
71
|
+
// Git: a pattern with a slash anywhere but the end is relative to the root (anchored);
|
|
72
|
+
// a pattern with no internal slash matches by name at any level.
|
|
73
|
+
const anchored = startSlash || p.includes('/');
|
|
74
|
+
let re = '';
|
|
75
|
+
for (let i = 0; i < p.length; i++) {
|
|
76
|
+
const c = p[i];
|
|
77
|
+
if (c === '*') {
|
|
78
|
+
if (p[i + 1] === '*') {
|
|
79
|
+
// `**` spans path segments. A segment-aligned `**/` becomes OPTIONAL leading
|
|
80
|
+
// directories that preserve the `/` boundary (so `**/logs` matches `a/logs` but
|
|
81
|
+
// NOT `mylogs`); a trailing/standalone `**` matches anything.
|
|
82
|
+
if (p[i + 2] === '/') {
|
|
83
|
+
re += '(?:.*/)?';
|
|
84
|
+
i += 2; // consume the second '*' and the '/'
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
re += '.*';
|
|
88
|
+
i += 1; // consume the second '*'
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
re += '[^/]*';
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
else if (c === '?') {
|
|
96
|
+
re += '[^/]';
|
|
97
|
+
}
|
|
98
|
+
else if ('.+^${}()|[]\\'.includes(c)) {
|
|
99
|
+
re += `\\${c}`;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
re += c;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// A trailing-slash directory (or a bare literal path) OWNS everything under it, so it
|
|
106
|
+
// gets a "…/.*" / "(?:/.*)?" tail. A wildcard-TERMINATED pattern (e.g. `docs/*`) is
|
|
107
|
+
// single-level per GitHub — it must NOT gain that tail, or it would over-match nested
|
|
108
|
+
// files (`docs/*` matches `docs/a.md` but not `docs/sub/a.md`).
|
|
109
|
+
const endsWithWildcard = /[*?]$/.test(p);
|
|
110
|
+
const body = dirOnly ? `${re}/.*` : endsWithWildcard ? re : `${re}(?:/.*)?`;
|
|
111
|
+
const full = anchored ? `^${body}$` : `^(?:.*/)?${body}$`;
|
|
112
|
+
// Last line of defence: refuse a compiled source that still contains several nullable
|
|
113
|
+
// `(?:.*/)?` groups. The collapse above should make this unreachable for any input, so
|
|
114
|
+
// reaching it means a pattern shape we did not anticipate — drop the rule (parseCodeowners
|
|
115
|
+
// catches this) rather than hand the engine something that might backtrack.
|
|
116
|
+
const nullableGroups = (full.match(/\(\?:\.\*\/\)\?/g) ?? []).length;
|
|
117
|
+
if (nullableGroups > 2) {
|
|
118
|
+
throw new Error('codeowners pattern too ambiguous to match safely');
|
|
119
|
+
}
|
|
120
|
+
return new RegExp(full);
|
|
121
|
+
}
|
|
122
|
+
// Parse CODEOWNERS text into ordered rules (order matters: the LAST matching rule wins).
|
|
123
|
+
function parseCodeowners(text) {
|
|
124
|
+
const rules = [];
|
|
125
|
+
for (const line of text.split('\n')) {
|
|
126
|
+
// Stop at MAX_RULES rather than compiling a hostile 100k-line file. GitHub itself only
|
|
127
|
+
// honours the first 1000 rules, so truncating is consistent with the real semantics.
|
|
128
|
+
if (rules.length >= MAX_RULES)
|
|
129
|
+
break;
|
|
130
|
+
const trimmed = line.trim();
|
|
131
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
132
|
+
continue;
|
|
133
|
+
// Strip an inline comment, then split on whitespace: first token = pattern, rest = owners.
|
|
134
|
+
const noComment = trimmed.split('#')[0].trim();
|
|
135
|
+
if (!noComment)
|
|
136
|
+
continue;
|
|
137
|
+
const parts = noComment.split(/\s+/);
|
|
138
|
+
const pattern = parts[0];
|
|
139
|
+
// Owners are the @-prefixed tokens (drop bare emails — not requestable as reviewers).
|
|
140
|
+
const owners = parts.slice(1).filter((o) => o.startsWith('@'));
|
|
141
|
+
if (owners.length === 0)
|
|
142
|
+
continue;
|
|
143
|
+
try {
|
|
144
|
+
rules.push({ re: globToRegExp(pattern), owners });
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
/* skip an un-compilable pattern rather than fail the whole file */
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return rules;
|
|
151
|
+
}
|
|
152
|
+
// Fetch + parse a repo's CODEOWNERS (cached). Returns null when there's no file / it can't
|
|
153
|
+
// be read. Never throws.
|
|
154
|
+
async function loadRules(token, owner, name, accountId) {
|
|
155
|
+
const key = `${accountId}:${owner}/${name}`;
|
|
156
|
+
const hit = cache.get(key);
|
|
157
|
+
if (hit && Date.now() - hit.at < CACHE_TTL_MS)
|
|
158
|
+
return hit.rules;
|
|
159
|
+
let rules = null;
|
|
160
|
+
try {
|
|
161
|
+
for (const path of CODEOWNERS_PATHS) {
|
|
162
|
+
const res = await ghRestGetContentRaw(token, `/repos/${owner}/${name}/contents/${path}`);
|
|
163
|
+
if (res.ok && res.text) {
|
|
164
|
+
// Truncate before parsing: the body length is chosen by whoever owns the repo.
|
|
165
|
+
rules = parseCodeowners(res.text.length > MAX_CODEOWNERS_BYTES
|
|
166
|
+
? res.text.slice(0, MAX_CODEOWNERS_BYTES)
|
|
167
|
+
: res.text);
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
// Any non-404 (403 org wall / 401) also means "give up on CODEOWNERS" — stop trying.
|
|
171
|
+
if (res.status !== 404)
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
rules = null;
|
|
177
|
+
}
|
|
178
|
+
cache.set(key, { rules, at: Date.now() });
|
|
179
|
+
return rules;
|
|
180
|
+
}
|
|
181
|
+
// Given a PR's changed paths, return the CODEOWNERS-derived owners (users + teams). The
|
|
182
|
+
// LAST matching rule for each file wins (git semantics); owners are unioned across files.
|
|
183
|
+
export async function getCodeownersMatch(token, owner, name, accountId, changedPaths) {
|
|
184
|
+
const empty = { logins: [], teams: [] };
|
|
185
|
+
if (changedPaths.length === 0)
|
|
186
|
+
return empty;
|
|
187
|
+
const rules = await loadRules(token, owner, name, accountId);
|
|
188
|
+
if (!rules || rules.length === 0)
|
|
189
|
+
return empty;
|
|
190
|
+
// Bound the total match work to MAX_PATHS_MATCHED × MAX_RULES regex tests. Both factors
|
|
191
|
+
// are attacker-influenceable (the PR's file count and the repo's CODEOWNERS), and this runs
|
|
192
|
+
// synchronously on the request thread of a single-threaded server.
|
|
193
|
+
const paths = changedPaths.length > MAX_PATHS_MATCHED
|
|
194
|
+
? changedPaths.slice(0, MAX_PATHS_MATCHED)
|
|
195
|
+
: changedPaths;
|
|
196
|
+
const ownerTokens = new Set();
|
|
197
|
+
for (const path of paths) {
|
|
198
|
+
let last = null;
|
|
199
|
+
for (const r of rules)
|
|
200
|
+
if (r.re.test(path))
|
|
201
|
+
last = r; // last match wins
|
|
202
|
+
if (last)
|
|
203
|
+
for (const o of last.owners)
|
|
204
|
+
ownerTokens.add(o);
|
|
205
|
+
}
|
|
206
|
+
const logins = [];
|
|
207
|
+
const teams = [];
|
|
208
|
+
for (const tok of ownerTokens) {
|
|
209
|
+
const handle = tok.slice(1); // drop '@'
|
|
210
|
+
if (handle.includes('/')) {
|
|
211
|
+
const slug = handle.slice(handle.indexOf('/') + 1);
|
|
212
|
+
if (slug)
|
|
213
|
+
teams.push({ slug, name: handle });
|
|
214
|
+
}
|
|
215
|
+
else if (handle) {
|
|
216
|
+
logins.push(handle);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return { logins, teams };
|
|
220
|
+
}
|
|
221
|
+
//# sourceMappingURL=codeowners.js.map
|