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,94 @@
|
|
|
1
|
+
// Emoji reactions — the ONLY database work the feature does.
|
|
2
|
+
//
|
|
3
|
+
// Reactions are never stored. This module exists for exactly one reason: turn the
|
|
4
|
+
// (kind, LOCAL id) pairs a client sends into GitHub node ids, and do it through a join that
|
|
5
|
+
// is scoped by accountId — so tenancy is STRUCTURAL rather than a check some handler has to
|
|
6
|
+
// remember.
|
|
7
|
+
//
|
|
8
|
+
// Why local ids on the wire at all, when GitHub's mutation wants a node id?
|
|
9
|
+
// • An unvalidated node-id list would be BOTH an existence oracle over other tenants' GitHub
|
|
10
|
+
// content AND a way to spend this account's GitHub quota on arbitrary nodes. Resolving
|
|
11
|
+
// local ids removes the class: a client cannot name an id it does not own, because the
|
|
12
|
+
// join simply returns nothing.
|
|
13
|
+
// • The local ids are already on CommentDetail / PrCommentDetail / ReviewDetail, so no read
|
|
14
|
+
// payload had to grow a field and no getter had to change.
|
|
15
|
+
//
|
|
16
|
+
// The three kinds live in THREE SEPARATE ID SPACES (`review_comments`, `pr_comments`,
|
|
17
|
+
// `reviews`) — the same hazard `ml_comment_labels` documents — so every lookup carries the
|
|
18
|
+
// kind and the three are queried separately. A bare numeric id means nothing on its own.
|
|
19
|
+
import { and, eq, inArray } from 'drizzle-orm';
|
|
20
|
+
import { db, schema } from './client.js';
|
|
21
|
+
const { pullRequests, prComments, reviewComments, reviews } = schema;
|
|
22
|
+
/**
|
|
23
|
+
* Resolve a batch of (kind, local id) pairs to GitHub node ids, scoped to `accountId`.
|
|
24
|
+
*
|
|
25
|
+
* A pair that does not resolve — unknown id, wrong kind, another tenant's row — is simply
|
|
26
|
+
* ABSENT from the result. It is never an error and never a 404: distinguishing "does not
|
|
27
|
+
* exist" from "is not yours" is the oracle this design avoids, and the client's honest
|
|
28
|
+
* rendering for both is the same (no reaction bar).
|
|
29
|
+
*
|
|
30
|
+
* Three indexed `IN` lookups at most, and only for the kinds actually present.
|
|
31
|
+
*/
|
|
32
|
+
export async function resolveReactionTargets(accountId, targets) {
|
|
33
|
+
const byKind = new Map();
|
|
34
|
+
for (const t of targets) {
|
|
35
|
+
if (!Number.isInteger(t.id) || t.id <= 0)
|
|
36
|
+
continue;
|
|
37
|
+
let set = byKind.get(t.kind);
|
|
38
|
+
if (!set) {
|
|
39
|
+
set = new Set();
|
|
40
|
+
byKind.set(t.kind, set);
|
|
41
|
+
}
|
|
42
|
+
set.add(t.id);
|
|
43
|
+
}
|
|
44
|
+
const out = [];
|
|
45
|
+
const reviewCommentIds = byKind.get('review_comment');
|
|
46
|
+
if (reviewCommentIds && reviewCommentIds.size > 0) {
|
|
47
|
+
const rows = await db
|
|
48
|
+
.select({
|
|
49
|
+
id: reviewComments.id,
|
|
50
|
+
nodeId: reviewComments.githubNodeId,
|
|
51
|
+
prId: reviewComments.prId,
|
|
52
|
+
})
|
|
53
|
+
.from(reviewComments)
|
|
54
|
+
// The comment tables carry no accountId of their own — they reach their tenant through
|
|
55
|
+
// pr_id, exactly as ml_comment_labels' cleanup does. The join IS the isolation.
|
|
56
|
+
.innerJoin(pullRequests, eq(pullRequests.id, reviewComments.prId))
|
|
57
|
+
.where(and(eq(pullRequests.accountId, accountId), inArray(reviewComments.id, [...reviewCommentIds])))
|
|
58
|
+
.execute();
|
|
59
|
+
for (const r of rows) {
|
|
60
|
+
out.push({ kind: 'review_comment', id: r.id, nodeId: r.nodeId, prId: r.prId });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const prCommentIds = byKind.get('pr_comment');
|
|
64
|
+
if (prCommentIds && prCommentIds.size > 0) {
|
|
65
|
+
const rows = await db
|
|
66
|
+
.select({ id: prComments.id, nodeId: prComments.githubNodeId, prId: prComments.prId })
|
|
67
|
+
.from(prComments)
|
|
68
|
+
.innerJoin(pullRequests, eq(pullRequests.id, prComments.prId))
|
|
69
|
+
.where(and(eq(pullRequests.accountId, accountId), inArray(prComments.id, [...prCommentIds])))
|
|
70
|
+
.execute();
|
|
71
|
+
for (const r of rows) {
|
|
72
|
+
out.push({ kind: 'pr_comment', id: r.id, nodeId: r.nodeId, prId: r.prId });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const reviewIds = byKind.get('review');
|
|
76
|
+
if (reviewIds && reviewIds.size > 0) {
|
|
77
|
+
const rows = await db
|
|
78
|
+
.select({ id: reviews.id, nodeId: reviews.githubNodeId, prId: reviews.prId })
|
|
79
|
+
.from(reviews)
|
|
80
|
+
.innerJoin(pullRequests, eq(pullRequests.id, reviews.prId))
|
|
81
|
+
.where(and(eq(pullRequests.accountId, accountId), inArray(reviews.id, [...reviewIds])))
|
|
82
|
+
.execute();
|
|
83
|
+
for (const r of rows) {
|
|
84
|
+
out.push({ kind: 'review', id: r.id, nodeId: r.nodeId, prId: r.prId });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
/** Single-target convenience for the write route. `null` ⇒ unknown or not this account's. */
|
|
90
|
+
export async function resolveReactionTarget(accountId, target) {
|
|
91
|
+
const rows = await resolveReactionTargets(accountId, [target]);
|
|
92
|
+
return rows[0] ?? null;
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=reaction-targets.js.map
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// WHERE IS THE WORK HAPPENING — per-repository activity over a rolling 14 days.
|
|
2
|
+
//
|
|
3
|
+
// The flow-metric tiles beside this on Reports → Overview answer "how much" and "how fast" for the
|
|
4
|
+
// whole workspace. They cannot answer "which repository", because a workspace-wide figure is one
|
|
5
|
+
// number over a population whose members routinely differ by two orders of magnitude: on the dev
|
|
6
|
+
// corpus one fortnight held 147 PRs / 1.4k lines in a config repo and 71 PRs / 141.7k lines in an
|
|
7
|
+
// application repo. Both are "activity"; they are not the same activity.
|
|
8
|
+
//
|
|
9
|
+
// ── WHY THIS IS A SEPARATE MODULE AND NOT A FIELD ON getWorkspaceMetrics ─────────────────────
|
|
10
|
+
//
|
|
11
|
+
// Two reasons, and the second is the load-bearing one:
|
|
12
|
+
//
|
|
13
|
+
// 1. `getWorkspaceMetrics` is a 400-line fold whose PR SELECT carries none of the five columns
|
|
14
|
+
// this needs (repoId, authorId, additions, deletions, changedFiles).
|
|
15
|
+
// 2. It honours an ARBITRARY comparison window handed in by the Pro layer (the workspace sprint
|
|
16
|
+
// cadence). This surface is FREE and cannot read that cadence — `resolveComparisonWindow` /
|
|
17
|
+
// `getComparisonWindow` live in the private plugin over `pro_workspace_settings`, and CORE has
|
|
18
|
+
// zero references to them. So the window here is the same trailing 14 days
|
|
19
|
+
// (`INSIGHT_SPRINT_DAYS`) the free tiles already use, which is what makes the chart and the
|
|
20
|
+
// tiles beside it agree BY CONSTRUCTION rather than by coincidence. Folding this into
|
|
21
|
+
// `getWorkspaceMetrics` would silently hand it the plugin's cadence window on the one call
|
|
22
|
+
// path that has one, and the panel would then carry two 14-day-labelled things measuring
|
|
23
|
+
// different fortnights.
|
|
24
|
+
//
|
|
25
|
+
// It also cannot live in `queries.ts`: it needs `resolveActorLanes`, and `actor-lanes.ts` imports
|
|
26
|
+
// FROM `queries.ts`. Every other lane-consuming fold (`period-metrics.ts`, `pr-intervals.ts`,
|
|
27
|
+
// `automation-output.ts`, `person-period.ts`) is a module beside them for exactly that reason.
|
|
28
|
+
//
|
|
29
|
+
// COST: two indexed scans (one PR window, one repo list) plus `resolveActorLanes`' fixed handful.
|
|
30
|
+
// Nothing multiplies by repo count, which is what keeps `/api/workspace-metrics` honest on the
|
|
31
|
+
// `read` fall-through — the tier comment in api/plugins/rate-limit.ts turns on that word.
|
|
32
|
+
import { and, eq, gte, inArray, lt } from 'drizzle-orm';
|
|
33
|
+
import { db, schema } from './client.js';
|
|
34
|
+
import { resolveActorLanes } from './actor-lanes.js';
|
|
35
|
+
const { pullRequests, repos } = schema;
|
|
36
|
+
/** The window. MIRRORS `INSIGHT_SPRINT_DAYS` in queries.ts, which is what the free flow-metric
|
|
37
|
+
* tiles use — the two are read side by side and a divergence would be invisible. Deliberately a
|
|
38
|
+
* constant and not a parameter: this is a FREE surface and the sprint cadence is plugin-owned, so
|
|
39
|
+
* there is no setting that could legitimately move it. The label on screen must say "rolling 14
|
|
40
|
+
* days", never "this sprint". */
|
|
41
|
+
export const REPO_ACTIVITY_WINDOW_DAYS = 14;
|
|
42
|
+
/** Top-N by PRs opened. Beyond about a dozen bands the bars are unreadable even rotated (the dev
|
|
43
|
+
* workspace has 19 active repositories in a typical fortnight). NEVER a silent truncation — the
|
|
44
|
+
* response carries `activeRepos` and an `omitted` aggregate so the cards can state both what was
|
|
45
|
+
* cut and what it was worth. */
|
|
46
|
+
export const REPO_ACTIVITY_MAX_REPOS = 12;
|
|
47
|
+
/**
|
|
48
|
+
* Per-repository PR and line counts over the trailing `REPO_ACTIVITY_WINDOW_DAYS`.
|
|
49
|
+
*
|
|
50
|
+
* ⚠ `scope.repoIds` IS REQUIRED AND CONCRETE, and `[]` returns null — the same contract
|
|
51
|
+
* `getWorkspaceMetricsForScope` states at length. A nullable "means every repo" parameter would
|
|
52
|
+
* widen an empty workspace to the whole account, which is the opposite of what a scope is.
|
|
53
|
+
* `scope.workspaceId` is not decoration either: it is the JUDGEMENT grain, the thing that decides
|
|
54
|
+
* who counts as automation, and it is why this takes a `BotScope` rather than a bare id list.
|
|
55
|
+
*/
|
|
56
|
+
export async function getWorkspaceRepoActivity(accountId, scope, nowMs) {
|
|
57
|
+
if (scope.repoIds.length === 0)
|
|
58
|
+
return null;
|
|
59
|
+
// Ownership narrowing, mirroring getWorkspaceMetricsForScope: `resolveWorkspaceScope` already
|
|
60
|
+
// guarantees `repoIds ⊆ this workspace's membership`, and this second predicate makes the tenancy
|
|
61
|
+
// structural in the query rather than inherited from a caller's memory.
|
|
62
|
+
const ownedRepos = await db
|
|
63
|
+
.select({ id: repos.id, owner: repos.owner, name: repos.name, createdAt: repos.createdAt })
|
|
64
|
+
.from(repos)
|
|
65
|
+
.where(and(eq(repos.accountId, accountId), inArray(repos.id, scope.repoIds)))
|
|
66
|
+
.execute();
|
|
67
|
+
if (ownedRepos.length === 0)
|
|
68
|
+
return null;
|
|
69
|
+
const ownedIds = ownedRepos.map((r) => r.id);
|
|
70
|
+
const toMs = nowMs;
|
|
71
|
+
const fromMs = toMs - REPO_ACTIVITY_WINDOW_DAYS * 86_400_000;
|
|
72
|
+
// ⚠ TWO-SIDED AND HALF-OPEN, `[fromMs, toMs)`. `gte`/`lt`, never `lte` on the upper bound: a PR
|
|
73
|
+
// opened at exactly `toMs` belongs to the next read of this window, not to both.
|
|
74
|
+
const prs = await db
|
|
75
|
+
.select({
|
|
76
|
+
repoId: pullRequests.repoId,
|
|
77
|
+
authorId: pullRequests.authorId,
|
|
78
|
+
additions: pullRequests.additions,
|
|
79
|
+
deletions: pullRequests.deletions,
|
|
80
|
+
changedFiles: pullRequests.changedFiles,
|
|
81
|
+
})
|
|
82
|
+
.from(pullRequests)
|
|
83
|
+
.where(and(eq(pullRequests.accountId, accountId), inArray(pullRequests.repoId, ownedIds), gte(pullRequests.openedAt, new Date(fromMs)), lt(pullRequests.openedAt, new Date(toMs))))
|
|
84
|
+
.execute();
|
|
85
|
+
// THE AUTOMATION SET. `automatedIds`, never `automatedReviewerUserIds` alone. Real accounts carry
|
|
86
|
+
// the same actor as two rows with conflicting flags (`dependabot` and `dependabot[bot]`), and on
|
|
87
|
+
// the measured account one of each pair sat at `automated: 0`. Trusting one signal splits one bot
|
|
88
|
+
// across both series and under-counts each.
|
|
89
|
+
//
|
|
90
|
+
// ⚠ ITS CANDIDATE SET IS (workspace verdict ∪ globally `users.isBot`), and the login vocabularies
|
|
91
|
+
// refine a lane INSIDE that set rather than widening it — a vocabulary hit counts on its own for
|
|
92
|
+
// a candidate, but admits nobody. So an actor with a vendor login, `isBot: false` and no
|
|
93
|
+
// `workspace_reviewers` row lands in the HUMAN series. That is deliberate: it is the answer every
|
|
94
|
+
// other lane consumer gives, and re-deriving "is this a bot" here to be cleverer would put a
|
|
95
|
+
// second classifier on screen that can disagree with the Timeline's own bot-hiding.
|
|
96
|
+
const lanes = await resolveActorLanes(accountId, scope);
|
|
97
|
+
const byRepo = new Map();
|
|
98
|
+
for (const p of prs) {
|
|
99
|
+
let acc = byRepo.get(p.repoId);
|
|
100
|
+
if (!acc) {
|
|
101
|
+
acc = { human: 0, automation: 0, lines: 0, sized: 0, unsized: 0 };
|
|
102
|
+
byRepo.set(p.repoId, acc);
|
|
103
|
+
}
|
|
104
|
+
// A null author is neither evidence of a bot nor of a person; it counts as human for the same
|
|
105
|
+
// reason `resolveActorLanes` rule 2 does — the absence of an automation signal is not one.
|
|
106
|
+
if (p.authorId != null && lanes.automatedIds.has(p.authorId))
|
|
107
|
+
acc.automation += 1;
|
|
108
|
+
else
|
|
109
|
+
acc.human += 1;
|
|
110
|
+
// ⚠ THE SANCTIONED UNSIZED GUARD, copied verbatim from period-metrics.ts. The three size
|
|
111
|
+
// columns are NOT NULL DEFAULT 0, so a PR whose detail never hydrated is byte-identical to one
|
|
112
|
+
// that changed nothing. Summing the fabricated zero is invisible; summing 80% of a repository's
|
|
113
|
+
// PRs and calling it that repository's line count is a wrong number with no way to notice.
|
|
114
|
+
const observed = p.changedFiles > 0 || p.additions > 0 || p.deletions > 0;
|
|
115
|
+
if (observed) {
|
|
116
|
+
acc.sized += 1;
|
|
117
|
+
acc.lines += p.additions + p.deletions;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
acc.unsized += 1;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const rows = [];
|
|
124
|
+
for (const r of ownedRepos) {
|
|
125
|
+
const acc = byRepo.get(r.id);
|
|
126
|
+
if (!acc)
|
|
127
|
+
continue; // a repository with nothing opened in the window draws no band at all
|
|
128
|
+
rows.push({
|
|
129
|
+
repoId: r.id,
|
|
130
|
+
repoFullName: `${r.owner}/${r.name}`,
|
|
131
|
+
prsOpenedHuman: acc.human,
|
|
132
|
+
prsOpenedAutomation: acc.automation,
|
|
133
|
+
linesChanged: acc.sized > 0 ? acc.lines : null,
|
|
134
|
+
sizedPrs: acc.sized,
|
|
135
|
+
unsizedPrs: acc.unsized,
|
|
136
|
+
// ⚠ `repos.createdAt` is when the repo was added to THIS ACCOUNT, not when it was created on
|
|
137
|
+
// GitHub — which is exactly the axis wanted: it bounds how much of the window we could
|
|
138
|
+
// possibly have observed. A real `>=` against the window start; sqlite stores these as epoch
|
|
139
|
+
// SECONDS, so no `+1ms` fudging.
|
|
140
|
+
addedDuringWindow: r.createdAt.getTime() >= fromMs,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
// Rank by TOTAL PRs opened — the primary metric, and the one both charts are ordered by so a
|
|
144
|
+
// reader can scan the same repository across the pair without re-finding it. Ties break on the
|
|
145
|
+
// repo name so the order is stable between requests rather than heap order.
|
|
146
|
+
rows.sort((a, b) => b.prsOpenedHuman +
|
|
147
|
+
b.prsOpenedAutomation -
|
|
148
|
+
(a.prsOpenedHuman + a.prsOpenedAutomation) ||
|
|
149
|
+
a.repoFullName.localeCompare(b.repoFullName));
|
|
150
|
+
const shown = rows.slice(0, REPO_ACTIVITY_MAX_REPOS);
|
|
151
|
+
const cut = rows.slice(REPO_ACTIVITY_MAX_REPOS);
|
|
152
|
+
// What the cap cut, so the cards can state it. The lines chart is ranked by the PR count, so the
|
|
153
|
+
// repository that leads on lines CAN be below the fold — `omitted.linesChanged` is the only thing
|
|
154
|
+
// that lets the reader see that happened. Null when nothing cut was ever sized, for the same
|
|
155
|
+
// reason a row's own figure is: unknown is not zero.
|
|
156
|
+
const cutSized = cut.reduce((n, r) => n + r.sizedPrs, 0);
|
|
157
|
+
return {
|
|
158
|
+
windowDays: REPO_ACTIVITY_WINDOW_DAYS,
|
|
159
|
+
from: new Date(fromMs).toISOString(),
|
|
160
|
+
to: new Date(toMs).toISOString(),
|
|
161
|
+
repos: shown,
|
|
162
|
+
activeRepos: rows.length,
|
|
163
|
+
workspaceRepos: ownedRepos.length,
|
|
164
|
+
omitted: {
|
|
165
|
+
repos: cut.length,
|
|
166
|
+
prsOpened: cut.reduce((n, r) => n + r.prsOpenedHuman + r.prsOpenedAutomation, 0),
|
|
167
|
+
linesChanged: cutSized > 0 ? cut.reduce((n, r) => n + (r.linesChanged ?? 0), 0) : null,
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=repo-activity.js.map
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { inArray, lt } from 'drizzle-orm';
|
|
2
|
+
import { db, runTransaction, schema } from './client.js';
|
|
3
|
+
import { config } from '../config.js';
|
|
4
|
+
// Time-based retention sweep. Per-account server data (events, PRs + their whole subtree)
|
|
5
|
+
// accumulates forever otherwise; this prunes anything past the retention window so the DB
|
|
6
|
+
// stays bounded (the pre-launch, tight-budget scalability lever). Runs periodically from
|
|
7
|
+
// the scheduler in BOTH deployment modes.
|
|
8
|
+
//
|
|
9
|
+
// THE ONE SAFE TTL KEY is `pullRequests.updatedAt < cutoff`, and we delete the WHOLE PR
|
|
10
|
+
// subtree anchored to that. Two load-bearing reasons:
|
|
11
|
+
// 1. Idempotency: the incremental sync re-walks a window ending at `lastIncremental −
|
|
12
|
+
// overlap` (~20 min) and a forced full sync re-walks `now − backfillDays`. A PR whose
|
|
13
|
+
// updatedAt is beyond the (clamped ≥ backfillDays) cutoff is outside BOTH windows, so
|
|
14
|
+
// it is never re-fetched → stays deleted. Keying on a CHILD's own timestamp instead
|
|
15
|
+
// (an old commit/event on a still-recent PR) would let the next sync re-create it →
|
|
16
|
+
// churn. So we anchor everything to the parent PR's recency.
|
|
17
|
+
// 2. FK integrity: there is NO `ON DELETE CASCADE` anywhere (FKs ARE enforced), so the
|
|
18
|
+
// subtree is deleted manually, parent-last, in a transaction — mirroring deleteRepo.
|
|
19
|
+
const { events, ciStatusEvents, reviewComments, reviewThreads, prComments, reviews, commits, reviewRequests, prViews, claudeReviews, claudeReviewFindings, pullRequests, prMentions, } = schema;
|
|
20
|
+
const retentionHandlers = [];
|
|
21
|
+
export function registerRetentionHandler(h) {
|
|
22
|
+
retentionHandlers.push(h);
|
|
23
|
+
}
|
|
24
|
+
// Batch size for the delete — bounds each transaction + the IN-clause size on both dialects.
|
|
25
|
+
const BATCH = 500;
|
|
26
|
+
function chunk(arr, n) {
|
|
27
|
+
const out = [];
|
|
28
|
+
for (let i = 0; i < arr.length; i += n)
|
|
29
|
+
out.push(arr.slice(i, i + n));
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
// Delete a batch of PRs and everything hanging off them, FK-safe order (children before
|
|
33
|
+
// parents — the exact order deleteRepo uses). `tx` is the dialect-aware transaction executor.
|
|
34
|
+
async function deletePrSubtree(tx, prIds) {
|
|
35
|
+
await tx.delete(events).where(inArray(events.prId, prIds)).execute();
|
|
36
|
+
// CI status history FKs pull_requests with ON DELETE no action (migrations 0022 / pg
|
|
37
|
+
// 0011) — it MUST go before the pullRequests delete below or the sweep FK-fails.
|
|
38
|
+
await tx.delete(ciStatusEvents).where(inArray(ciStatusEvents.prId, prIds)).execute();
|
|
39
|
+
await tx.delete(reviewComments).where(inArray(reviewComments.prId, prIds)).execute();
|
|
40
|
+
await tx.delete(reviewThreads).where(inArray(reviewThreads.prId, prIds)).execute();
|
|
41
|
+
await tx.delete(prComments).where(inArray(prComments.prId, prIds)).execute();
|
|
42
|
+
await tx.delete(reviews).where(inArray(reviews.prId, prIds)).execute();
|
|
43
|
+
await tx.delete(commits).where(inArray(commits.prId, prIds)).execute();
|
|
44
|
+
await tx.delete(reviewRequests).where(inArray(reviewRequests.prId, prIds)).execute();
|
|
45
|
+
// Review-request HISTORY (migration 0066 / pg 0053) — a PR child with no cascade; it must go
|
|
46
|
+
// before the pullRequests delete below or the sweep FK-fails.
|
|
47
|
+
await tx
|
|
48
|
+
.delete(schema.reviewRequestEvents)
|
|
49
|
+
.where(inArray(schema.reviewRequestEvents.prId, prIds))
|
|
50
|
+
.execute();
|
|
51
|
+
await tx.delete(prViews).where(inArray(prViews.prId, prIds)).execute();
|
|
52
|
+
// "@you" mention rows (migration 0056 / pg 0043). The pr_id FK cascades, but this sweep runs
|
|
53
|
+
// parent-last by hand on both dialects and a mention row outliving its PR would go on claiming
|
|
54
|
+
// a deleted PR is personally relevant.
|
|
55
|
+
await tx.delete(prMentions).where(inArray(prMentions.prId, prIds)).execute();
|
|
56
|
+
// My Turn dismissals of these PRs (migration 0069 / pg 0056) — the composite FK cascades, but this
|
|
57
|
+
// sweep runs parent-last by hand on both dialects.
|
|
58
|
+
await tx
|
|
59
|
+
.delete(schema.myTurnDismissals)
|
|
60
|
+
.where(inArray(schema.myTurnDismissals.prId, prIds))
|
|
61
|
+
.execute();
|
|
62
|
+
// Claude review runs + findings FK these PRs — clear findings (via reviewId) then runs.
|
|
63
|
+
const reviewIdRows = await tx
|
|
64
|
+
.select({ id: claudeReviews.id })
|
|
65
|
+
.from(claudeReviews)
|
|
66
|
+
.where(inArray(claudeReviews.prId, prIds))
|
|
67
|
+
.execute();
|
|
68
|
+
const reviewIds = reviewIdRows.map((r) => r.id);
|
|
69
|
+
if (reviewIds.length > 0) {
|
|
70
|
+
// Claude Review chat messages (migration 0073 / pg 0060) FK both the runs and the findings —
|
|
71
|
+
// they go first.
|
|
72
|
+
await tx
|
|
73
|
+
.delete(schema.claudeReviewChatMessages)
|
|
74
|
+
.where(inArray(schema.claudeReviewChatMessages.reviewId, reviewIds))
|
|
75
|
+
.execute();
|
|
76
|
+
await tx
|
|
77
|
+
.delete(claudeReviewFindings)
|
|
78
|
+
.where(inArray(claudeReviewFindings.reviewId, reviewIds))
|
|
79
|
+
.execute();
|
|
80
|
+
}
|
|
81
|
+
await tx.delete(claudeReviews).where(inArray(claudeReviews.prId, prIds)).execute();
|
|
82
|
+
// AI Fix runs (core since migration 0074 / pg 0061; the plugin's retention hook used to prune
|
|
83
|
+
// them). No FKs — explicit, pr-keyed.
|
|
84
|
+
await tx.delete(schema.aiFixes).where(inArray(schema.aiFixes.prId, prIds)).execute();
|
|
85
|
+
await tx.delete(pullRequests).where(inArray(pullRequests.id, prIds)).execute();
|
|
86
|
+
// NB: commitFiles is GLOBAL (sha-keyed, shared across PRs/tenants) — deliberately NOT
|
|
87
|
+
// pruned here. users + syncState are likewise out of scope.
|
|
88
|
+
//
|
|
89
|
+
// NB2: `trunk_ci_status_events` (migration 0052 / pg 0039) is deliberately absent, and its
|
|
90
|
+
// absence is STRUCTURAL, not an oversight. This whole sweep is anchored to a parent PR's
|
|
91
|
+
// `updatedAt` (see the header) and a trunk CI observation has no PR — there is nothing here to
|
|
92
|
+
// key it on, and inventing a second, differently-keyed sweep would give the file two retention
|
|
93
|
+
// definitions. It is bounded instead by a HYBRID trim in its own writer
|
|
94
|
+
// (`sync/branch-status.ts` — `staleTrunkCiEventIds`: the newest `TRUNK_CI_EVENT_WINDOW` rows per
|
|
95
|
+
// repo ∪ everything inside `FEED_WINDOW_DAYS`), which runs on every branch snapshot, and it is
|
|
96
|
+
// cleared outright by `deleteRepo` + `eraseAccountData`. ⚠ It is NOT a plain count trim: a
|
|
97
|
+
// pure newest-N bound evicted the failure rows the Feed reads on repos that sync faster than the
|
|
98
|
+
// read window elapses. `branch_commits` is out for the same reason (its hybrid trim lives in the
|
|
99
|
+
// same writer).
|
|
100
|
+
}
|
|
101
|
+
// Prune every PR (and subtree) older than the retention window. `retentionDays` defaults
|
|
102
|
+
// to config; a test can pass a value. Returns the number of PRs pruned. No-op when
|
|
103
|
+
// retention is disabled (days <= 0).
|
|
104
|
+
export async function pruneOldData(log, retentionDays = config.retentionDays) {
|
|
105
|
+
if (!retentionDays || retentionDays <= 0)
|
|
106
|
+
return 0;
|
|
107
|
+
// Clamp to the backfill window so a forced full sync can't resurrect a deleted PR.
|
|
108
|
+
const effDays = Math.max(retentionDays, config.backfillDays);
|
|
109
|
+
const cutoff = new Date(Date.now() - effDays * 24 * 60 * 60 * 1000);
|
|
110
|
+
const oldRows = await db
|
|
111
|
+
.select({ id: pullRequests.id })
|
|
112
|
+
.from(pullRequests)
|
|
113
|
+
.where(lt(pullRequests.updatedAt, cutoff))
|
|
114
|
+
.execute();
|
|
115
|
+
const prIds = oldRows.map((r) => r.id);
|
|
116
|
+
if (prIds.length === 0)
|
|
117
|
+
return 0;
|
|
118
|
+
let pruned = 0;
|
|
119
|
+
for (const batch of chunk(prIds, BATCH)) {
|
|
120
|
+
await runTransaction(async (tx) => {
|
|
121
|
+
await deletePrSubtree(tx, batch);
|
|
122
|
+
});
|
|
123
|
+
// Prune plugin-owned tables for these PRs (outside the core tx — the plugin owns its
|
|
124
|
+
// own transactions; a plugin failure must not roll back the core delete). Best-effort.
|
|
125
|
+
for (const h of retentionHandlers) {
|
|
126
|
+
try {
|
|
127
|
+
await h({ prIds: batch });
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
log.warn({ err }, 'retention: pro handler failed for a batch');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
pruned += batch.length;
|
|
134
|
+
}
|
|
135
|
+
log.info(`retention: pruned ${pruned} PR(s) older than ${effDays}d`);
|
|
136
|
+
return pruned;
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=retention.js.map
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// CLAUDE REVIEW — the OTHER reviewers' open threads on a PR (CORE, DB-only, no GitHub call).
|
|
2
|
+
//
|
|
3
|
+
// Two readers, ONE predicate:
|
|
4
|
+
// • the review run loads every unresolved review thread that is NOT rooted on one of Limn's own
|
|
5
|
+
// posted findings (`loadReviewThreadsForReview`) and asks Claude, per thread, whether the
|
|
6
|
+
// comment is right and whether the code has dealt with it (review/claude-review/threads.ts);
|
|
7
|
+
// • the auto-review sweeper re-reviews the SAME head when a newer qualifying comment arrives
|
|
8
|
+
// (`newestReviewCommentAt`, read by `getAutoReviewCandidates`).
|
|
9
|
+
//
|
|
10
|
+
// ⚠ A QUALIFYING COMMENT is a review-thread comment in an UNRESOLVED thread that Limn did NOT post,
|
|
11
|
+
// by a person or a review bot (the `countsAuthor` filter below). Both readers go through `isLimnPostedComment`, so the comment
|
|
12
|
+
// the run records as "seen" (`claude_reviews.comments_through`) and the comment the sweeper treats
|
|
13
|
+
// as "new" can never disagree; if they did, a review could re-trigger itself forever.
|
|
14
|
+
//
|
|
15
|
+
// ⚠ HOW WE KNOW LIMN POSTED IT. Limn posts AS THE READER (their own GitHub token), so TWO facts
|
|
16
|
+
// must hold together:
|
|
17
|
+
// A. the comment's author is the ACCOUNT'S OWN GitHub login (accounts.github_login, compared
|
|
18
|
+
// case-insensitively) — a marker someone else pasted, or a teammate's own Limn post, is
|
|
19
|
+
// another reviewer's comment; and
|
|
20
|
+
// B. it carries Limn's provenance — any one of:
|
|
21
|
+
// 1. the hidden `<!-- pierre:claude-review` marker. Every finding comment carries
|
|
22
|
+
// `<!-- pierre:claude-review-finding v=1 -->` (post-review.ts FINDING_COMMENT_MARKER), review
|
|
23
|
+
// bodies their own spelling of it. It survives an edit on GitHub. ⚠ The retired per-ticket
|
|
24
|
+
// "Post as comment" stamped `<!-- pierre:claude-review-ticket v=1 -->`; nothing produces it
|
|
25
|
+
// now (a story's results post as findings), but comments already on GitHub carry it, so
|
|
26
|
+
// the match stays a PREFIX — never narrow it to the finding spelling;
|
|
27
|
+
// 2. LEGACY, for comments posted before that marker existed: the comment's GitHub id is a
|
|
28
|
+
// posted finding's `github_comment_id`, or its text STARTS WITH a posted finding's resolved
|
|
29
|
+
// body (an edit on GitHub defeats this one — hence the marker).
|
|
30
|
+
// An unknown author (a deleted account, an unsynced user) is never "own".
|
|
31
|
+
//
|
|
32
|
+
// ⚠ ONLY REAL REVIEWERS MOVE THE TRIGGER. The comment-time half (`newestReviewCommentAt`, and the
|
|
33
|
+
// loader's `newestCommentAt` that becomes `comments_through`) also takes a `countsAuthor` filter —
|
|
34
|
+
// built ONCE per workspace by db/queries.ts `reReviewCommentAuthorFilter`: a person, or a bot whose
|
|
35
|
+
// role in this workspace is `'review'`. CI, coverage, deploy and dependency bots never re-trigger a
|
|
36
|
+
// review. Both readers get the SAME filter, so what a run records as seen and what the sweeper
|
|
37
|
+
// calls new still agree. ⚠ The THREADS themselves are not filtered: the review still assesses
|
|
38
|
+
// every open thread, whoever wrote it.
|
|
39
|
+
import { and, eq, gt, inArray, isNotNull } from 'drizzle-orm';
|
|
40
|
+
import { db, schema } from './client.js';
|
|
41
|
+
import { globalAutomationUserIds } from './automation-ids.js';
|
|
42
|
+
const LIMN_MARKER = /<!--\s*pierre:claude-review/i;
|
|
43
|
+
const norm = (s) => s.replace(/\r\n?/g, '\n').trim();
|
|
44
|
+
/** The account's own GitHub login — the author half of `isLimnPostedComment`. */
|
|
45
|
+
export async function accountGithubLogin(accountId) {
|
|
46
|
+
const row = (await db
|
|
47
|
+
.select({ login: schema.accounts.githubLogin })
|
|
48
|
+
.from(schema.accounts)
|
|
49
|
+
.where(eq(schema.accounts.id, accountId))
|
|
50
|
+
.limit(1)
|
|
51
|
+
.execute())[0];
|
|
52
|
+
return row?.login ?? null;
|
|
53
|
+
}
|
|
54
|
+
/** Is this review-thread comment one Limn posted? (The ONE predicate — see the header.) */
|
|
55
|
+
export function isLimnPostedComment(c, own, accountLogin) {
|
|
56
|
+
if (!accountLogin || !c.authorLogin)
|
|
57
|
+
return false;
|
|
58
|
+
if (c.authorLogin.toLowerCase() !== accountLogin.toLowerCase())
|
|
59
|
+
return false;
|
|
60
|
+
const body = c.body ?? '';
|
|
61
|
+
if (LIMN_MARKER.test(body))
|
|
62
|
+
return true;
|
|
63
|
+
if (!own)
|
|
64
|
+
return false;
|
|
65
|
+
if (c.databaseId != null && own.commentIds.has(c.databaseId))
|
|
66
|
+
return true;
|
|
67
|
+
const text = norm(body);
|
|
68
|
+
if (!text)
|
|
69
|
+
return false;
|
|
70
|
+
return own.bodies.some((b) => b.length > 0 && text.startsWith(b));
|
|
71
|
+
}
|
|
72
|
+
/** Every POSTED finding's resolved body + stored comment id, per PR (account-scoped). */
|
|
73
|
+
export async function ownPostedCommentsForPrs(accountId, prIds) {
|
|
74
|
+
const out = new Map();
|
|
75
|
+
if (prIds.length === 0)
|
|
76
|
+
return out;
|
|
77
|
+
const { claudeReviews: cr, claudeReviewFindings: crf } = schema;
|
|
78
|
+
const rows = await db
|
|
79
|
+
.select({
|
|
80
|
+
prId: cr.prId,
|
|
81
|
+
body: crf.body,
|
|
82
|
+
editedBody: crf.editedBody,
|
|
83
|
+
githubCommentId: crf.githubCommentId,
|
|
84
|
+
})
|
|
85
|
+
.from(crf)
|
|
86
|
+
.innerJoin(cr, eq(cr.id, crf.reviewId))
|
|
87
|
+
.where(and(eq(cr.accountId, accountId), inArray(cr.prId, [...prIds]), isNotNull(crf.postedAt)))
|
|
88
|
+
.execute();
|
|
89
|
+
for (const r of rows) {
|
|
90
|
+
let o = out.get(r.prId);
|
|
91
|
+
if (!o) {
|
|
92
|
+
o = { bodies: [], commentIds: new Set() };
|
|
93
|
+
out.set(r.prId, o);
|
|
94
|
+
}
|
|
95
|
+
const resolved = r.editedBody && r.editedBody.trim() ? r.editedBody : r.body;
|
|
96
|
+
o.bodies.push(norm(resolved));
|
|
97
|
+
if (r.githubCommentId)
|
|
98
|
+
o.commentIds.add(r.githubCommentId);
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The newest QUALIFYING review-thread comment per PR, created strictly after `since` (a lower bound
|
|
104
|
+
* that keeps the scan small; pass the oldest cutoff you care about). PRs with none are absent.
|
|
105
|
+
*/
|
|
106
|
+
export async function newestReviewCommentAt(accountId, prIds, since,
|
|
107
|
+
// TRAILING: only these authors' comments count (see the header). Absent ⇒ every author.
|
|
108
|
+
countsAuthor) {
|
|
109
|
+
const out = new Map();
|
|
110
|
+
if (prIds.length === 0)
|
|
111
|
+
return out;
|
|
112
|
+
const { reviewComments: rc, reviewThreads: rt, pullRequests: prs, users } = schema;
|
|
113
|
+
const rows = await db
|
|
114
|
+
.select({
|
|
115
|
+
prId: rc.prId,
|
|
116
|
+
createdAt: rc.createdAt,
|
|
117
|
+
body: rc.body,
|
|
118
|
+
databaseId: rc.databaseId,
|
|
119
|
+
authorId: rc.authorId,
|
|
120
|
+
authorLogin: users.githubLogin,
|
|
121
|
+
})
|
|
122
|
+
.from(rc)
|
|
123
|
+
.innerJoin(rt, eq(rt.id, rc.threadId))
|
|
124
|
+
.innerJoin(prs, eq(prs.id, rc.prId))
|
|
125
|
+
.leftJoin(users, eq(users.id, rc.authorId))
|
|
126
|
+
.where(and(eq(prs.accountId, accountId), inArray(rc.prId, [...prIds]), eq(rt.isResolved, false), gt(rc.createdAt, since)))
|
|
127
|
+
.execute();
|
|
128
|
+
if (rows.length === 0)
|
|
129
|
+
return out;
|
|
130
|
+
const [own, login] = await Promise.all([
|
|
131
|
+
ownPostedCommentsForPrs(accountId, [...new Set(rows.map((r) => r.prId))]),
|
|
132
|
+
accountGithubLogin(accountId),
|
|
133
|
+
]);
|
|
134
|
+
for (const r of rows) {
|
|
135
|
+
if (isLimnPostedComment(r, own.get(r.prId), login))
|
|
136
|
+
continue;
|
|
137
|
+
if (countsAuthor && !countsAuthor(r.authorId ?? null))
|
|
138
|
+
continue;
|
|
139
|
+
const prev = out.get(r.prId);
|
|
140
|
+
if (!prev || r.createdAt.getTime() > prev.getTime())
|
|
141
|
+
out.set(r.prId, r.createdAt);
|
|
142
|
+
}
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Every unresolved review thread on the PR that is NOT rooted on one of Limn's own posted findings,
|
|
147
|
+
* with its comments. Account-scoped (a foreign PR reads as no threads). `authorIsBot` is the GLOBAL
|
|
148
|
+
* automation set (`globalAutomationUserIds`) — the ball rule's set, which needs no workspace.
|
|
149
|
+
*/
|
|
150
|
+
export async function loadReviewThreadsForReview(accountId, prId,
|
|
151
|
+
// TRAILING: which authors move `newestCommentAt` (never which threads are sent). Absent ⇒ all.
|
|
152
|
+
countsAuthor) {
|
|
153
|
+
const { reviewComments: rc, reviewThreads: rt, pullRequests: prs, repos, users, commits } = schema;
|
|
154
|
+
const pr = (await db
|
|
155
|
+
.select({ number: prs.number, owner: repos.owner, name: repos.name })
|
|
156
|
+
.from(prs)
|
|
157
|
+
.innerJoin(repos, eq(repos.id, prs.repoId))
|
|
158
|
+
.where(and(eq(prs.id, prId), eq(prs.accountId, accountId)))
|
|
159
|
+
.limit(1)
|
|
160
|
+
.execute())[0];
|
|
161
|
+
if (!pr)
|
|
162
|
+
return { threads: [], newestCommentAt: null };
|
|
163
|
+
const rows = await db
|
|
164
|
+
.select({
|
|
165
|
+
threadId: rt.id,
|
|
166
|
+
path: rt.path,
|
|
167
|
+
line: rt.line,
|
|
168
|
+
isOutdated: rt.isOutdated,
|
|
169
|
+
derivedState: rt.derivedState,
|
|
170
|
+
commentId: rc.id,
|
|
171
|
+
body: rc.body,
|
|
172
|
+
excerpt: rc.excerpt,
|
|
173
|
+
databaseId: rc.databaseId,
|
|
174
|
+
createdAt: rc.createdAt,
|
|
175
|
+
authorId: rc.authorId,
|
|
176
|
+
authorLogin: users.githubLogin,
|
|
177
|
+
})
|
|
178
|
+
.from(rt)
|
|
179
|
+
.innerJoin(rc, eq(rc.threadId, rt.id))
|
|
180
|
+
.leftJoin(users, eq(users.id, rc.authorId))
|
|
181
|
+
.where(and(eq(rt.prId, prId), eq(rt.isResolved, false)))
|
|
182
|
+
.execute();
|
|
183
|
+
if (rows.length === 0)
|
|
184
|
+
return { threads: [], newestCommentAt: null };
|
|
185
|
+
const [ownMap, bots, commitRows, login] = await Promise.all([
|
|
186
|
+
ownPostedCommentsForPrs(accountId, [prId]),
|
|
187
|
+
globalAutomationUserIds(),
|
|
188
|
+
db.select({ committedAt: commits.committedAt }).from(commits).where(eq(commits.prId, prId)).execute(),
|
|
189
|
+
accountGithubLogin(accountId),
|
|
190
|
+
]);
|
|
191
|
+
const own = ownMap.get(prId);
|
|
192
|
+
const commitTimes = commitRows.map((c) => c.committedAt.getTime());
|
|
193
|
+
const after = (t) => commitTimes.filter((c) => c > t.getTime()).length;
|
|
194
|
+
const byThread = new Map();
|
|
195
|
+
for (const r of rows) {
|
|
196
|
+
const list = byThread.get(r.threadId) ?? [];
|
|
197
|
+
list.push(r);
|
|
198
|
+
byThread.set(r.threadId, list);
|
|
199
|
+
}
|
|
200
|
+
let newest = null;
|
|
201
|
+
const threads = [];
|
|
202
|
+
for (const list of byThread.values()) {
|
|
203
|
+
list.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime() || a.commentId - b.commentId);
|
|
204
|
+
const root = list[0];
|
|
205
|
+
const rootIsOwn = isLimnPostedComment(root, own, login);
|
|
206
|
+
const kept = list.filter((c) => !isLimnPostedComment(c, own, login));
|
|
207
|
+
for (const c of kept) {
|
|
208
|
+
if (countsAuthor && !countsAuthor(c.authorId ?? null))
|
|
209
|
+
continue;
|
|
210
|
+
if (!newest || c.createdAt > newest)
|
|
211
|
+
newest = c.createdAt;
|
|
212
|
+
}
|
|
213
|
+
// A thread rooted on Limn's own posted finding belongs to the follow-up, not here.
|
|
214
|
+
if (rootIsOwn || kept.length === 0)
|
|
215
|
+
continue;
|
|
216
|
+
const first = kept[0];
|
|
217
|
+
const last = kept[kept.length - 1];
|
|
218
|
+
threads.push({
|
|
219
|
+
threadId: root.threadId,
|
|
220
|
+
path: root.path,
|
|
221
|
+
line: root.line,
|
|
222
|
+
isOutdated: root.isOutdated,
|
|
223
|
+
derivedState: root.derivedState,
|
|
224
|
+
url: root.databaseId
|
|
225
|
+
? `https://github.com/${pr.owner}/${pr.name}/pull/${pr.number}#discussion_r${root.databaseId}`
|
|
226
|
+
: null,
|
|
227
|
+
comments: kept.map((c) => ({
|
|
228
|
+
authorLogin: c.authorLogin ?? null,
|
|
229
|
+
authorIsBot: c.authorId != null && bots.has(c.authorId),
|
|
230
|
+
createdAt: c.createdAt,
|
|
231
|
+
body: (c.body ?? c.excerpt ?? '').trim(),
|
|
232
|
+
})),
|
|
233
|
+
commitsAfterFirstComment: after(first.createdAt),
|
|
234
|
+
commitsAfterLastComment: after(last.createdAt),
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
threads.sort((a, b) => a.threadId - b.threadId);
|
|
238
|
+
return { threads, newestCommentAt: newest };
|
|
239
|
+
}
|
|
240
|
+
//# sourceMappingURL=review-threads-for-review.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { db, isPg } from './client.js';
|
|
3
|
+
// Apply pending migrations against the shared connection. Dialect-aware: the
|
|
4
|
+
// sqlite migrator is synchronous, the node-postgres migrator is async — so this
|
|
5
|
+
// is `async` either way and the caller (index.ts / migrate.ts) awaits it. Each
|
|
6
|
+
// dialect resolves its own migrations folder next to the compiled output
|
|
7
|
+
// (dist/db/migrations or dist/db/migrations-pg). Safe at server startup — does
|
|
8
|
+
// not close the connection.
|
|
9
|
+
export async function runMigrations() {
|
|
10
|
+
const migrationsFolder = resolve(import.meta.dirname, isPg ? 'migrations-pg' : 'migrations');
|
|
11
|
+
if (isPg) {
|
|
12
|
+
const { migrate } = await import('drizzle-orm/node-postgres/migrator');
|
|
13
|
+
await migrate(db, {
|
|
14
|
+
migrationsFolder,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
const { migrate } = await import('drizzle-orm/better-sqlite3/migrator');
|
|
19
|
+
// `db` is typed as the pg instance; at runtime here it's the better-sqlite3
|
|
20
|
+
// one, which is what the sqlite migrator expects.
|
|
21
|
+
migrate(db, { migrationsFolder });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=run-migrations.js.map
|