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,563 @@
|
|
|
1
|
+
import { and, eq, inArray } from 'drizzle-orm';
|
|
2
|
+
import { db, schema } from '../../db/client.js';
|
|
3
|
+
import { getGraphqlClientFor, graphqlTolerant } from '../../github/client.js';
|
|
4
|
+
import { getAccessToken } from '../../auth/account.js';
|
|
5
|
+
import { OWNER_TYPE_QUERY, REPO_ID_QUERY, REPO_SEARCH_QUERY, VIEWER_REPOS_QUERY, } from '../../github/queries.js';
|
|
6
|
+
import { upsertRepo } from '../../sync/upsert.js';
|
|
7
|
+
import { enqueueSyncForRepo, getSyncStatus, isSyncQueued, isSyncRunning, listActiveSyncProgress, requestSyncCancel, manualSyncCooldownMs, noteManualSync, waitForSyncToStop, } from '../../sync/sync-manager.js';
|
|
8
|
+
import { deleteRepo, getAddedRepoNodeIds, getRepo, listRepos, resolveWorkspaceScope, } from '../../db/queries.js';
|
|
9
|
+
import { getBranchStatus, getBranchTrends } from '../../db/branch-queries.js';
|
|
10
|
+
import { withPrChangeSignal } from '../../sync/pr-change-signal.js';
|
|
11
|
+
import { accountIdOf } from '../plugins/auth.js';
|
|
12
|
+
// Local copy of the shared MAX_REPOS_PER_ACCOUNT value. `@pierre-review/shared` is
|
|
13
|
+
// a types-only package (not shipped in the published tarball), so the backend must
|
|
14
|
+
// only `import type` from it — runtime constants are duplicated here. Keep in sync.
|
|
15
|
+
const MAX_REPOS_PER_ACCOUNT = 100;
|
|
16
|
+
const createRepoSchema = {
|
|
17
|
+
body: {
|
|
18
|
+
type: 'object',
|
|
19
|
+
required: ['owner', 'name'],
|
|
20
|
+
additionalProperties: false,
|
|
21
|
+
properties: {
|
|
22
|
+
owner: { type: 'string', minLength: 1 },
|
|
23
|
+
name: { type: 'string', minLength: 1 },
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
const idParamSchema = {
|
|
28
|
+
params: {
|
|
29
|
+
type: 'object',
|
|
30
|
+
required: ['id'],
|
|
31
|
+
properties: { id: { type: 'integer' } },
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
const syncSchema = {
|
|
35
|
+
...idParamSchema,
|
|
36
|
+
querystring: {
|
|
37
|
+
type: 'object',
|
|
38
|
+
additionalProperties: false,
|
|
39
|
+
properties: { full: { type: 'boolean', default: false } },
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
const searchSchema = {
|
|
43
|
+
querystring: {
|
|
44
|
+
type: 'object',
|
|
45
|
+
required: ['q'],
|
|
46
|
+
additionalProperties: false,
|
|
47
|
+
properties: {
|
|
48
|
+
q: { type: 'string', minLength: 1, maxLength: 256 },
|
|
49
|
+
cursor: { type: 'string', minLength: 1 },
|
|
50
|
+
limit: { type: 'integer', default: 10, minimum: 1, maximum: 25 },
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
// `repoIds=1,2,3` → [1,2,3]; absent/blank/garbage → null ("no explicit narrowing"). It is fed to
|
|
55
|
+
// `resolveWorkspaceScope` as the `narrow` argument, never used as a scope in its own right.
|
|
56
|
+
function parseIntList(raw) {
|
|
57
|
+
if (!raw)
|
|
58
|
+
return null;
|
|
59
|
+
const ids = raw
|
|
60
|
+
.split(',')
|
|
61
|
+
.map((s) => Number.parseInt(s.trim(), 10))
|
|
62
|
+
.filter((n) => Number.isFinite(n));
|
|
63
|
+
return ids.length > 0 ? ids : null;
|
|
64
|
+
}
|
|
65
|
+
export async function repoRoutes(app) {
|
|
66
|
+
// `lastPrChangeAt` rides the listing so the SPA's existing 30s `['repos']` poll learns about a
|
|
67
|
+
// PR change made OUTSIDE a walk (a webhook, a post-write settle, a backstop re-read) — see
|
|
68
|
+
// sync/pr-change-signal.ts. In-memory, account-keyed, optional on the wire.
|
|
69
|
+
app.get('/api/repos', async (req) => {
|
|
70
|
+
const accountId = accountIdOf(req);
|
|
71
|
+
return withPrChangeSignal(accountId, await listRepos(accountId));
|
|
72
|
+
});
|
|
73
|
+
// Default-branch status ("is trunk green?") for every repo in the active WORKSPACE: the head
|
|
74
|
+
// snapshot plus the recent trunk commits with their own CI state. A pure DB read off what the
|
|
75
|
+
// branch sync already persisted — never a live GitHub call, hence the plain `read` rate-limit
|
|
76
|
+
// tier. Informational only: nothing here feeds attention counts, My Turn, or any badge.
|
|
77
|
+
//
|
|
78
|
+
// `?workspace=<id>` is the scope; `?repoIds=` only narrows within it. It carried repo ids alone
|
|
79
|
+
// while `null` meant "every repo of the account", which under a workspace scope meant an EMPTY
|
|
80
|
+
// workspace (no ids on the wire) rendered the whole account's trunk strip, and two workspaces
|
|
81
|
+
// sending no ids shared one client cache slot.
|
|
82
|
+
//
|
|
83
|
+
// ⚠ THE EMPTY CASE IS HANDLED HERE, NOT IN THE GETTER. `getBranchStatus` reads `[]` as "every
|
|
84
|
+
// repo the account owns" — the same widening this route exists to close — so an empty workspace
|
|
85
|
+
// must short-circuit before the call. That getter lives in db/branch-queries.ts and is not this
|
|
86
|
+
// unit's to change; the guard is the route's until it is.
|
|
87
|
+
app.get('/api/branch-status', async (req) => {
|
|
88
|
+
const q = req.query;
|
|
89
|
+
const accountId = accountIdOf(req);
|
|
90
|
+
const scope = await resolveWorkspaceScope(accountId, q.workspace, parseIntList(q.repoIds));
|
|
91
|
+
if (scope.repoIds.length === 0)
|
|
92
|
+
return { repos: [] };
|
|
93
|
+
return getBranchStatus(accountId, scope.repoIds);
|
|
94
|
+
});
|
|
95
|
+
// The lazy per-day trend series behind an expanded default-branch row (failing trunk commits
|
|
96
|
+
// + PRs merged into the default branch, one shared axis). Fetched only on expand, so it takes
|
|
97
|
+
// ONE repo — never the workspace scope — and, unlike /api/branch-status, the id-addressed
|
|
98
|
+
// getter enforces ownership (→ 404). A pure DB read: default `read` tier.
|
|
99
|
+
app.get('/api/branch-trends', {
|
|
100
|
+
schema: {
|
|
101
|
+
querystring: {
|
|
102
|
+
type: 'object',
|
|
103
|
+
required: ['repoId'],
|
|
104
|
+
additionalProperties: false,
|
|
105
|
+
properties: { repoId: { type: 'integer' } },
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
}, async (req, reply) => {
|
|
109
|
+
const { repoId } = req.query;
|
|
110
|
+
const trends = await getBranchTrends(accountIdOf(req), repoId);
|
|
111
|
+
if (trends == null) {
|
|
112
|
+
reply.status(404);
|
|
113
|
+
return { error: 'NotFound', message: `Repo ${repoId} not found` };
|
|
114
|
+
}
|
|
115
|
+
return trends;
|
|
116
|
+
});
|
|
117
|
+
// Live GitHub repository search for the Add-repo picker. Best-match ordering
|
|
118
|
+
// (GitHub default), repos already added to this account filtered out, owned/member repos
|
|
119
|
+
// floated to the top. Detail comes straight from GitHub — nothing persisted.
|
|
120
|
+
app.get('/api/repos/search', { schema: searchSchema }, async (req, reply) => {
|
|
121
|
+
const { q, cursor, limit } = req.query;
|
|
122
|
+
const term = q.trim();
|
|
123
|
+
if (!term) {
|
|
124
|
+
reply.status(400);
|
|
125
|
+
return { error: 'BadRequest', message: 'Search query must not be empty' };
|
|
126
|
+
}
|
|
127
|
+
const accountId = accountIdOf(req);
|
|
128
|
+
// Translate the user term into a literal GitHub search query. An `owner/...`
|
|
129
|
+
// prefix scopes results to that owner (org:/user: qualifier — resolved by type);
|
|
130
|
+
// the remainder (or the whole term) is matched against the repo NAME only
|
|
131
|
+
// (`in:name`), and `needle` drives the literal re-rank below.
|
|
132
|
+
const client = getGraphqlClientFor(await getAccessToken(accountId));
|
|
133
|
+
const slash = term.indexOf('/');
|
|
134
|
+
const owner = slash >= 0 ? term.slice(0, slash).trim() : '';
|
|
135
|
+
const rest = slash >= 0 ? term.slice(slash + 1).trim() : term;
|
|
136
|
+
let searchQuery;
|
|
137
|
+
let needle;
|
|
138
|
+
if (owner) {
|
|
139
|
+
// `owner/...` → scope to that owner. Note a stray leading slash ("/foo") leaves
|
|
140
|
+
// owner empty and falls through to the plain branch (no malformed `user:` query).
|
|
141
|
+
needle = rest;
|
|
142
|
+
let qualifier = `user:${owner}`;
|
|
143
|
+
try {
|
|
144
|
+
const ownerResp = await client(OWNER_TYPE_QUERY, {
|
|
145
|
+
login: owner,
|
|
146
|
+
});
|
|
147
|
+
const kind = ownerResp.repositoryOwner?.__typename ?? null;
|
|
148
|
+
if (kind == null) {
|
|
149
|
+
// Owner login doesn't exist → no possible matches.
|
|
150
|
+
return { results: [], hasNextPage: false, cursor: null };
|
|
151
|
+
}
|
|
152
|
+
qualifier = kind === 'Organization' ? `org:${owner}` : `user:${owner}`;
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
// Lookup failed (network / rate limit) — default to user: scoping and run the
|
|
156
|
+
// search anyway. Logged so a wrong-scope result is diagnosable.
|
|
157
|
+
req.log.warn({ err, owner }, 'repo search: owner-type lookup failed; defaulting to user: scope');
|
|
158
|
+
}
|
|
159
|
+
searchQuery = rest ? `${qualifier} ${rest} in:name` : qualifier;
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
// Plain term (or a stray leading slash) → literal repo-name search.
|
|
163
|
+
needle = rest;
|
|
164
|
+
searchQuery = rest ? `${rest} in:name` : term;
|
|
165
|
+
}
|
|
166
|
+
let resp;
|
|
167
|
+
try {
|
|
168
|
+
// NB: the GraphQL variable is `searchQuery`, not `query` — @octokit/graphql
|
|
169
|
+
// reserves `query` for the document body and rejects it as a variable name.
|
|
170
|
+
resp = await client(REPO_SEARCH_QUERY, {
|
|
171
|
+
searchQuery,
|
|
172
|
+
first: limit,
|
|
173
|
+
cursor: cursor ?? null,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
reply.status(502);
|
|
178
|
+
return {
|
|
179
|
+
error: 'GitHubError',
|
|
180
|
+
message: err instanceof Error ? err.message : 'GitHub search failed',
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
const added = await getAddedRepoNodeIds(accountId);
|
|
184
|
+
const me = resp.viewer.login.toLowerCase();
|
|
185
|
+
const orgLogins = new Set(resp.viewer.organizations.nodes
|
|
186
|
+
.filter((o) => o != null)
|
|
187
|
+
.map((o) => o.login.toLowerCase()));
|
|
188
|
+
const results = resp.search.nodes
|
|
189
|
+
// GitHub returns a NULL node for any hit the token can't fully resolve (a scoped
|
|
190
|
+
// GitHub-App token in cloud hits this; the local `gh` PAT sees them all), and {} for
|
|
191
|
+
// a (theoretical) non-repo node. Drop both null-safely BEFORE reading `id`/`owner`,
|
|
192
|
+
// then drop the ones already added. (This null node is the cloud-only crash source.)
|
|
193
|
+
.filter((n) => n != null && typeof n.id === 'string')
|
|
194
|
+
.filter((n) => !added.has(n.id))
|
|
195
|
+
.map((n) => {
|
|
196
|
+
const ownerLogin = n.owner.login;
|
|
197
|
+
return {
|
|
198
|
+
githubNodeId: n.id,
|
|
199
|
+
owner: ownerLogin,
|
|
200
|
+
name: n.name,
|
|
201
|
+
fullName: n.nameWithOwner,
|
|
202
|
+
description: n.description,
|
|
203
|
+
ownerAvatarUrl: n.owner.avatarUrl,
|
|
204
|
+
stargazerCount: n.stargazerCount,
|
|
205
|
+
openPrCount: n.pullRequests.totalCount,
|
|
206
|
+
url: n.url,
|
|
207
|
+
isPrivate: n.isPrivate,
|
|
208
|
+
isOwnedOrMember: ownerLogin.toLowerCase() === me ||
|
|
209
|
+
orgLogins.has(ownerLogin.toLowerCase()),
|
|
210
|
+
};
|
|
211
|
+
});
|
|
212
|
+
// Re-rank for literal matching: closest name match first (exact < prefix <
|
|
213
|
+
// substring < other), then your own/org repos, then stars. Array.prototype.sort
|
|
214
|
+
// is stable (Node ≥ 12), so GitHub's best-match order breaks remaining ties.
|
|
215
|
+
const lc = needle.toLowerCase();
|
|
216
|
+
const nameTier = (name) => {
|
|
217
|
+
if (!lc)
|
|
218
|
+
return 0;
|
|
219
|
+
const n = name.toLowerCase();
|
|
220
|
+
if (n === lc)
|
|
221
|
+
return 0;
|
|
222
|
+
if (n.startsWith(lc))
|
|
223
|
+
return 1;
|
|
224
|
+
if (n.includes(lc))
|
|
225
|
+
return 2;
|
|
226
|
+
return 3;
|
|
227
|
+
};
|
|
228
|
+
results.sort((a, b) => {
|
|
229
|
+
const t = nameTier(a.name) - nameTier(b.name);
|
|
230
|
+
if (t !== 0)
|
|
231
|
+
return t;
|
|
232
|
+
const own = Number(b.isOwnedOrMember) - Number(a.isOwnedOrMember);
|
|
233
|
+
if (own !== 0)
|
|
234
|
+
return own;
|
|
235
|
+
return b.stargazerCount - a.stargazerCount;
|
|
236
|
+
});
|
|
237
|
+
const body = {
|
|
238
|
+
results,
|
|
239
|
+
hasNextPage: resp.search.pageInfo.hasNextPage,
|
|
240
|
+
cursor: resp.search.pageInfo.endCursor,
|
|
241
|
+
};
|
|
242
|
+
return body;
|
|
243
|
+
});
|
|
244
|
+
// First-run onboarding: the viewer's recently-active repositories, detected from their
|
|
245
|
+
// GitHub activity (recent pushes + contributions). Same token idiom as /search; repos
|
|
246
|
+
// already added filtered out; mapped to the picker's RepoSearchResult shape and ordered
|
|
247
|
+
// most-recently-pushed first, capped at 30. Detail comes straight from GitHub — nothing
|
|
248
|
+
// persisted. Local's broad `gh` token surfaces private + org repos; a scoped cloud token
|
|
249
|
+
// surfaces only what it can read (both are handled by the same null-tolerant merge below).
|
|
250
|
+
app.get('/api/repos/suggested', async (req, reply) => {
|
|
251
|
+
const accountId = accountIdOf(req);
|
|
252
|
+
const client = getGraphqlClientFor(await getAccessToken(accountId));
|
|
253
|
+
let resp;
|
|
254
|
+
try {
|
|
255
|
+
// Tolerate PARTIAL GraphQL errors (a scoped cloud token forbidden one sub-field answers
|
|
256
|
+
// 200 + partial data) — the null-drop merge below is built for exactly that. Only a
|
|
257
|
+
// response with NO usable data (auth failure, rate limit, network) 502s.
|
|
258
|
+
resp = await graphqlTolerant(client, VIEWER_REPOS_QUERY, {}, (errors) => req.log.warn({ errors }, 'partial viewer-repos response'));
|
|
259
|
+
}
|
|
260
|
+
catch (err) {
|
|
261
|
+
reply.status(502);
|
|
262
|
+
return {
|
|
263
|
+
error: 'GitHubError',
|
|
264
|
+
message: err instanceof Error ? err.message : 'GitHub request failed',
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
if (resp?.viewer == null)
|
|
268
|
+
return { results: [] };
|
|
269
|
+
const me = resp.viewer.login.toLowerCase();
|
|
270
|
+
const orgLogins = new Set(resp.viewer.organizations.nodes
|
|
271
|
+
.filter((o) => o != null)
|
|
272
|
+
.map((o) => o.login.toLowerCase()));
|
|
273
|
+
// Merge the two recency-ordered lists, deduping by repo node id and keeping the more
|
|
274
|
+
// recent pushedAt. Null node arrays (scoped token) and null nodes are dropped BEFORE any
|
|
275
|
+
// field read (ISO-8601 pushedAt strings compare lexically; a null pushedAt sorts last).
|
|
276
|
+
const byId = new Map();
|
|
277
|
+
const merged = [
|
|
278
|
+
...(resp.viewer.repositories.nodes ?? []),
|
|
279
|
+
...(resp.viewer.repositoriesContributedTo.nodes ?? []),
|
|
280
|
+
];
|
|
281
|
+
for (const n of merged) {
|
|
282
|
+
if (n == null || typeof n.id !== 'string')
|
|
283
|
+
continue;
|
|
284
|
+
const existing = byId.get(n.id);
|
|
285
|
+
if (existing == null || (n.pushedAt ?? '') > (existing.pushedAt ?? '')) {
|
|
286
|
+
byId.set(n.id, n);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const added = await getAddedRepoNodeIds(accountId);
|
|
290
|
+
const results = [...byId.values()]
|
|
291
|
+
.filter((n) => !added.has(n.id))
|
|
292
|
+
// Most-recently-pushed first; a missing pushedAt sorts to the bottom.
|
|
293
|
+
.sort((a, b) => (b.pushedAt ?? '').localeCompare(a.pushedAt ?? ''))
|
|
294
|
+
.slice(0, 30)
|
|
295
|
+
.map((n) => {
|
|
296
|
+
const ownerLogin = n.owner.login;
|
|
297
|
+
return {
|
|
298
|
+
githubNodeId: n.id,
|
|
299
|
+
owner: ownerLogin,
|
|
300
|
+
name: n.name,
|
|
301
|
+
fullName: n.nameWithOwner,
|
|
302
|
+
description: n.description,
|
|
303
|
+
ownerAvatarUrl: n.owner.avatarUrl,
|
|
304
|
+
stargazerCount: n.stargazerCount,
|
|
305
|
+
openPrCount: n.pullRequests.totalCount,
|
|
306
|
+
url: n.url,
|
|
307
|
+
isPrivate: n.isPrivate,
|
|
308
|
+
isOwnedOrMember: ownerLogin.toLowerCase() === me ||
|
|
309
|
+
orgLogins.has(ownerLogin.toLowerCase()),
|
|
310
|
+
};
|
|
311
|
+
});
|
|
312
|
+
const body = { results };
|
|
313
|
+
return body;
|
|
314
|
+
});
|
|
315
|
+
app.post('/api/repos', { schema: createRepoSchema }, async (req, reply) => {
|
|
316
|
+
const { owner, name } = req.body;
|
|
317
|
+
const accountId = accountIdOf(req);
|
|
318
|
+
let resp;
|
|
319
|
+
try {
|
|
320
|
+
const client = getGraphqlClientFor(await getAccessToken(accountId));
|
|
321
|
+
resp = await client(REPO_ID_QUERY, { owner, name });
|
|
322
|
+
}
|
|
323
|
+
catch (err) {
|
|
324
|
+
reply.status(502);
|
|
325
|
+
return {
|
|
326
|
+
error: 'GitHubError',
|
|
327
|
+
message: err instanceof Error ? err.message : 'GitHub request failed',
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
if (!resp.repository) {
|
|
331
|
+
reply.status(404);
|
|
332
|
+
return {
|
|
333
|
+
error: 'NotFound',
|
|
334
|
+
message: `Repository ${owner}/${name} not found or inaccessible. Check the name and your gh auth / SSO.`,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
// Enforce the per-account repo cap. Re-adding a repo that is already there is an
|
|
338
|
+
// idempotent no-op (it doesn't grow the count), so only genuinely NEW repos
|
|
339
|
+
// are blocked once at the limit.
|
|
340
|
+
const added = await getAddedRepoNodeIds(accountId);
|
|
341
|
+
if (added.size >= MAX_REPOS_PER_ACCOUNT && !added.has(resp.repository.id)) {
|
|
342
|
+
reply.status(409);
|
|
343
|
+
return {
|
|
344
|
+
error: 'RepoLimitExceeded',
|
|
345
|
+
message: `You can add at most ${MAX_REPOS_PER_ACCOUNT} repositories. Remove one to add another.`,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
const canonOwner = resp.repository.owner.login;
|
|
349
|
+
const canonName = resp.repository.name;
|
|
350
|
+
const repoId = await upsertRepo(canonOwner, canonName, resp.repository.id, null, accountId);
|
|
351
|
+
// No second visibility axis to set: `upsertRepo` gives the repo its workspace membership
|
|
352
|
+
// in the same transaction, and a repo in a workspace is fully live — Feed, Activity, My
|
|
353
|
+
// Turn and Bots all cover it. (The old `setRepoInboxWatch(accountId, repoId, true)` call
|
|
354
|
+
// that used to sit here, and the `watch` body field that fed it, are both gone.)
|
|
355
|
+
//
|
|
356
|
+
// My Turn's "New PRs" clock is `repos.createdAt`, stamped by that same upsert, so adding a
|
|
357
|
+
// repo with 400 open PRs still doesn't dump them into the inbox on day one.
|
|
358
|
+
// Kick off the initial backfill in the background — QUEUED behind any other
|
|
359
|
+
// API-triggered sync of this account, so adding several repos consecutively walks
|
|
360
|
+
// them one at a time instead of hammering one token with N concurrent 90-day
|
|
361
|
+
// backfills (each was also a 10-way commit-file REST fan-out). The 201 still
|
|
362
|
+
// returns immediately; the queued repo reports status 'running' with
|
|
363
|
+
// `paused: { reason: 'queued' }` until its turn comes.
|
|
364
|
+
void enqueueSyncForRepo(repoId, app.log).catch((err) => app.log.error(`enqueue initial sync for repo ${repoId} failed: ${err instanceof Error ? err.message : err}`));
|
|
365
|
+
reply.status(201);
|
|
366
|
+
return getRepo(repoId, accountId);
|
|
367
|
+
});
|
|
368
|
+
// NOTE: there is no `PATCH /api/repos/:id`. Its only body was `{ inboxWatch }`, and the
|
|
369
|
+
// "watched" axis is gone — a repo is either in a workspace (fully live) or removed. Moving
|
|
370
|
+
// a repo between workspaces is `PATCH /api/workspaces/:id` / `POST /api/workspaces/:id/repos`,
|
|
371
|
+
// not a mutation of the repo row.
|
|
372
|
+
app.delete('/api/repos/:id', { schema: idParamSchema }, async (req, reply) => {
|
|
373
|
+
const { id } = req.params;
|
|
374
|
+
const accountId = accountIdOf(req);
|
|
375
|
+
// Ownership check first: a repo this account doesn't own is a 404 (and we
|
|
376
|
+
// must not consult the repoId-keyed sync managers for someone else's repo).
|
|
377
|
+
if (!(await getRepo(id, accountId))) {
|
|
378
|
+
reply.status(404);
|
|
379
|
+
return { error: 'NotFound', message: `Repo ${id} not found` };
|
|
380
|
+
}
|
|
381
|
+
// A repo still WAITING in the per-account queue would resurrect the deleted repo
|
|
382
|
+
// when its turn came (the walk's upsertRepo re-creates the row). Cancelling a queued
|
|
383
|
+
// repo is synchronous: it dequeues + clears the progress row, so the delete can go.
|
|
384
|
+
if (isSyncQueued(id))
|
|
385
|
+
requestSyncCancel(id);
|
|
386
|
+
// A sync in flight would re-create the repo (and its rows) right after we
|
|
387
|
+
// delete them, since the sync's upserts are still running. Refuse until it
|
|
388
|
+
// settles — the cron tick / initial backfill is short.
|
|
389
|
+
if (isSyncRunning(id)) {
|
|
390
|
+
reply.status(409);
|
|
391
|
+
return {
|
|
392
|
+
error: 'Conflict',
|
|
393
|
+
message: 'A sync is running for this repo — try removing it again in a moment.',
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
const ok = await deleteRepo(id, accountId);
|
|
397
|
+
if (!ok) {
|
|
398
|
+
reply.status(404);
|
|
399
|
+
return { error: 'NotFound', message: `Repo ${id} not found` };
|
|
400
|
+
}
|
|
401
|
+
reply.status(204);
|
|
402
|
+
return null;
|
|
403
|
+
});
|
|
404
|
+
app.post('/api/repos/:id/sync', { schema: syncSchema }, async (req, reply) => {
|
|
405
|
+
const { id } = req.params;
|
|
406
|
+
if (!(await getRepo(id, accountIdOf(req)))) {
|
|
407
|
+
reply.status(404);
|
|
408
|
+
return { error: 'NotFound', message: `Repo ${id} not found` };
|
|
409
|
+
}
|
|
410
|
+
// ?full=true forces a full backfill (catches CI/thread-resolve changes
|
|
411
|
+
// that don't bump PR.updatedAt and so lag the incremental path).
|
|
412
|
+
const { full } = req.query;
|
|
413
|
+
const forceFull = full === true;
|
|
414
|
+
// Per-repo cooldown. `runSyncForRepo`'s own guard only refuses a sync already in
|
|
415
|
+
// flight for this repo — it did not stop a caller re-firing the moment one finished,
|
|
416
|
+
// so a loop could keep a forced 90-day backfill running permanently and drain the
|
|
417
|
+
// tenant's GitHub quota (breaking their real sync for the rest of the hour).
|
|
418
|
+
const waitMs = manualSyncCooldownMs(id, forceFull);
|
|
419
|
+
if (waitMs > 0) {
|
|
420
|
+
const retryAfter = Math.ceil(waitMs / 1000);
|
|
421
|
+
reply.status(429).header('Retry-After', String(retryAfter));
|
|
422
|
+
return {
|
|
423
|
+
error: 'TooManyRequests',
|
|
424
|
+
message: forceFull
|
|
425
|
+
? `A deep sync for this repo ran recently — try again in ${retryAfter}s.`
|
|
426
|
+
: `This repo was synced moments ago — try again in ${retryAfter}s.`,
|
|
427
|
+
retryAfter,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
// QUEUED behind the account's other API-triggered syncs (the per-account serial
|
|
431
|
+
// queue replaced the old process-wide concurrency 429): "deep sync everything"
|
|
432
|
+
// fires one POST per repo, and each repo now waits its turn with an honest
|
|
433
|
+
// 'queued' progress row instead of either running 100 concurrent GraphQL walks
|
|
434
|
+
// or bouncing off a 429. Returns as soon as the repo is queued.
|
|
435
|
+
const started = await enqueueSyncForRepo(id, app.log, { forceFull });
|
|
436
|
+
if (!started) {
|
|
437
|
+
reply.status(409);
|
|
438
|
+
return { error: 'Conflict', message: 'A sync is already running for this repo' };
|
|
439
|
+
}
|
|
440
|
+
noteManualSync(id);
|
|
441
|
+
reply.status(202);
|
|
442
|
+
return { status: 'started' };
|
|
443
|
+
});
|
|
444
|
+
app.get('/api/repos/:id/sync-status', { schema: idParamSchema }, async (req, reply) => {
|
|
445
|
+
const { id } = req.params;
|
|
446
|
+
if (!(await getRepo(id, accountIdOf(req)))) {
|
|
447
|
+
reply.status(404);
|
|
448
|
+
return { error: 'NotFound', message: `Repo ${id} not found` };
|
|
449
|
+
}
|
|
450
|
+
return getSyncStatus(id);
|
|
451
|
+
});
|
|
452
|
+
// A cold return's catch-up: an incremental walk whose cutoff is older than this is fetching
|
|
453
|
+
// a delta measured in DAYS, not the ~50 minutes a routine tick covers (cold activity bucket
|
|
454
|
+
// 15 min + the 30-min forced re-walk floor + the 20-min overlap). Three orders of magnitude
|
|
455
|
+
// between the two populations, so nothing here needs tuning.
|
|
456
|
+
const CATCHUP_MIN_AGE_MS = 24 * 60 * 60 * 1000;
|
|
457
|
+
// The account's HEAVY sync work, in TWO disjoint arrays:
|
|
458
|
+
// • `backfills` — repos whose CURRENT walk is full-mode: a first-sync backfill, a forced
|
|
459
|
+
// deep re-sync, or a repo QUEUED for one (its seeded progress row rides
|
|
460
|
+
// `paused: { reason: 'queued' }` and counts only when the seeded mode is 'full').
|
|
461
|
+
// • `catchups` — the ONE kind of incremental walk that is reported: one whose `since` is
|
|
462
|
+
// more than 24h old, i.e. somebody came back to the app after days away and the board is
|
|
463
|
+
// that far behind. Routine incremental ticks stay EXCLUDED: this feeds the SPA's global
|
|
464
|
+
// bottom-right loading bar, which must not flicker on every cron tick.
|
|
465
|
+
// They are separate arrays rather than a flag because they are separate populations and the
|
|
466
|
+
// bar says a different sentence about each ("Backfilling" is a repo's first 90 days;
|
|
467
|
+
// "Catching up" is an established repo that is behind).
|
|
468
|
+
//
|
|
469
|
+
// Cost: an in-memory snapshot (`listActiveSyncProgress`) + ONE account-scoped `repos`
|
|
470
|
+
// SELECT. The join is not just for `fullName` — the progress maps are process-wide across
|
|
471
|
+
// tenants in cloud, so the accountId predicate IS the isolation: a repo that doesn't come
|
|
472
|
+
// back from the SELECT (another tenant's, or deleted mid-walk) is silently dropped. Pure
|
|
473
|
+
// DB read + memory, default `read` rate tier.
|
|
474
|
+
app.get('/api/sync-activity', async (req) => {
|
|
475
|
+
const accountId = accountIdOf(req);
|
|
476
|
+
const now = Date.now();
|
|
477
|
+
const generatedAt = new Date(now).toISOString();
|
|
478
|
+
// Returns the cutoff when this walk is a catch-up, else null — one predicate, used for
|
|
479
|
+
// both the filter and the row, so a row can never be classified one way and read another.
|
|
480
|
+
// An absent `sinceMs` is "not recorded", never "old": it must not invent a catch-up.
|
|
481
|
+
const catchupSinceMs = (p) => p.mode === 'incremental' && p.sinceMs != null && now - p.sinceMs > CATCHUP_MIN_AGE_MS
|
|
482
|
+
? p.sinceMs
|
|
483
|
+
: null;
|
|
484
|
+
const heavy = listActiveSyncProgress().filter((e) => e.progress.mode === 'full' || catchupSinceMs(e.progress) != null);
|
|
485
|
+
if (heavy.length === 0)
|
|
486
|
+
return { backfills: [], catchups: [], generatedAt };
|
|
487
|
+
const { repos, syncState } = schema;
|
|
488
|
+
const rows = await db
|
|
489
|
+
.select({
|
|
490
|
+
id: repos.id,
|
|
491
|
+
owner: repos.owner,
|
|
492
|
+
name: repos.name,
|
|
493
|
+
lastSyncStatus: syncState.lastSyncStatus,
|
|
494
|
+
})
|
|
495
|
+
.from(repos)
|
|
496
|
+
.leftJoin(syncState, eq(syncState.repoId, repos.id))
|
|
497
|
+
.where(and(eq(repos.accountId, accountId), inArray(repos.id, heavy.map((e) => e.repoId))))
|
|
498
|
+
.execute();
|
|
499
|
+
const nameById = new Map(rows.map((r) => [r.id, `${r.owner}/${r.name}`]));
|
|
500
|
+
const erroredIds = new Set(rows.filter((r) => r.lastSyncStatus === 'error').map((r) => r.id));
|
|
501
|
+
const backfills = [];
|
|
502
|
+
const catchups = [];
|
|
503
|
+
for (const { repoId, progress } of heavy) {
|
|
504
|
+
const fullName = nameById.get(repoId);
|
|
505
|
+
if (fullName === undefined)
|
|
506
|
+
continue; // not this account's repo (or deleted mid-walk)
|
|
507
|
+
// A permanently failing first backfill (revoked token, SAML-walled org) is retried
|
|
508
|
+
// by the scheduler indefinitely, each attempt seeding a fresh full-mode row — which
|
|
509
|
+
// would flash the ambient loading bar on an idle board forever. Suppress the
|
|
510
|
+
// zero-progress retries of a repo already marked errored; a walk that is actually
|
|
511
|
+
// making progress (percent > 0) or an explicit user-queued re-sync still shows.
|
|
512
|
+
//
|
|
513
|
+
// ⚠ THIS COVERS CATCH-UPS TOO, AND FOR A SHARPER REASON: a repo whose walk fails every
|
|
514
|
+
// time never stamps lastIncrementalSyncAt, so its `since` gets older every attempt and
|
|
515
|
+
// it would read as a catch-up FOREVER — a permanent "18 days behind" on a repo nothing
|
|
516
|
+
// can fetch. The predicate is mode-blind on purpose; do not narrow it to full-mode.
|
|
517
|
+
if (erroredIds.has(repoId) &&
|
|
518
|
+
progress.percent === 0 &&
|
|
519
|
+
progress.paused?.reason !== 'queued') {
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
const row = {
|
|
523
|
+
repoId,
|
|
524
|
+
fullName,
|
|
525
|
+
percent: progress.percent,
|
|
526
|
+
prsProcessed: progress.prsProcessed,
|
|
527
|
+
...(progress.paused ? { paused: progress.paused } : {}),
|
|
528
|
+
};
|
|
529
|
+
const sinceMs = catchupSinceMs(progress);
|
|
530
|
+
if (sinceMs != null)
|
|
531
|
+
catchups.push({ ...row, sinceMs });
|
|
532
|
+
else
|
|
533
|
+
backfills.push(row);
|
|
534
|
+
}
|
|
535
|
+
return { backfills, catchups, generatedAt };
|
|
536
|
+
});
|
|
537
|
+
// Cancel an in-flight sync. Signals the sync loop to stop, waits for it to
|
|
538
|
+
// settle, then — if this repo never completed a sync (an initial backfill the
|
|
539
|
+
// user is aborting) — deletes it and its partially-loaded data. An established
|
|
540
|
+
// repo whose re-sync was cancelled keeps everything. Drives the modal's Cancel.
|
|
541
|
+
app.post('/api/repos/:id/cancel', { schema: idParamSchema }, async (req, reply) => {
|
|
542
|
+
const { id } = req.params;
|
|
543
|
+
const accountId = accountIdOf(req);
|
|
544
|
+
if (!(await getRepo(id, accountId))) {
|
|
545
|
+
reply.status(404);
|
|
546
|
+
return { error: 'NotFound', message: `Repo ${id} not found` };
|
|
547
|
+
}
|
|
548
|
+
requestSyncCancel(id);
|
|
549
|
+
await waitForSyncToStop(id, 30_000);
|
|
550
|
+
// Re-read AFTER it stops: if the sync actually finished during the wait, the
|
|
551
|
+
// repo is now "synced" and must NOT be deleted (avoids a cancel-vs-finish race).
|
|
552
|
+
const fresh = await getRepo(id, accountId);
|
|
553
|
+
const neverSynced = fresh != null &&
|
|
554
|
+
fresh.lastFullSyncAt == null &&
|
|
555
|
+
fresh.lastIncrementalSyncAt == null;
|
|
556
|
+
let deleted = false;
|
|
557
|
+
if (neverSynced && !isSyncRunning(id))
|
|
558
|
+
deleted = await deleteRepo(id, accountId);
|
|
559
|
+
reply.status(200);
|
|
560
|
+
return { repoId: id, deleted };
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
//# sourceMappingURL=repos.js.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { resolveWorkspaceScope } from '../../db/queries.js';
|
|
2
|
+
import { searchPrs } from '../../db/search.js';
|
|
3
|
+
import { accountIdOf } from '../plugins/auth.js';
|
|
4
|
+
// Workspace-wide full-text search (CORE, no AI). GET /api/search?q=&workspace=&kinds=&limit=&offset=
|
|
5
|
+
// searches the local `search_index` (PR titles + descriptions, review bodies, review-comments,
|
|
6
|
+
// PR-comments, and authors) across the active workspace's repos. `workspace` is a plain integer;
|
|
7
|
+
// absent / unparseable / another tenant's id all resolve to the account's DEFAULT workspace (never
|
|
8
|
+
// a 404 — every id yields the same response shape, so it is not an existence oracle). It is
|
|
9
|
+
// resolved to a repo set server-side (`resolveWorkspaceScope`) so a caller can't widen it, and an
|
|
10
|
+
// empty workspace resolves to `[]` → no hits, rather than the whole account. `kinds`
|
|
11
|
+
// (comma-separated) optionally narrows to one or more hit kinds. Account-scoped in the query layer
|
|
12
|
+
// → no cross-tenant leak.
|
|
13
|
+
const VALID_KINDS = new Set(['pr', 'review', 'review_comment', 'pr_comment']);
|
|
14
|
+
const MAX_LIMIT = 50;
|
|
15
|
+
const DEFAULT_LIMIT = 25;
|
|
16
|
+
export async function searchRoutes(app) {
|
|
17
|
+
app.get('/api/search', async (req) => {
|
|
18
|
+
const q = req.query;
|
|
19
|
+
const accountId = accountIdOf(req);
|
|
20
|
+
const query = (q.q ?? '').trim();
|
|
21
|
+
if (query === '')
|
|
22
|
+
return { query: '', hits: [], people: [], total: 0 };
|
|
23
|
+
const scope = await resolveWorkspaceScope(accountId, q.workspace);
|
|
24
|
+
const kinds = (q.kinds ?? '')
|
|
25
|
+
.split(',')
|
|
26
|
+
.map((k) => k.trim())
|
|
27
|
+
.filter((k) => VALID_KINDS.has(k));
|
|
28
|
+
const limRaw = Number.parseInt(q.limit ?? '', 10);
|
|
29
|
+
const offRaw = Number.parseInt(q.offset ?? '', 10);
|
|
30
|
+
const limit = Number.isFinite(limRaw) && limRaw > 0 ? Math.min(limRaw, MAX_LIMIT) : DEFAULT_LIMIT;
|
|
31
|
+
const offset = Number.isFinite(offRaw) && offRaw > 0 ? offRaw : 0;
|
|
32
|
+
return searchPrs(accountId, {
|
|
33
|
+
query,
|
|
34
|
+
repoIds: scope.repoIds,
|
|
35
|
+
kinds: kinds.length > 0 ? kinds : undefined,
|
|
36
|
+
limit,
|
|
37
|
+
offset,
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=search.js.map
|