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,317 @@
|
|
|
1
|
+
import { and, desc, eq, inArray, or, sql } from 'drizzle-orm';
|
|
2
|
+
import { db, schema, runTransaction } from './client.js';
|
|
3
|
+
// Cross-repo text search over the local `search_index` (CORE, no AI), scoped to ONE workspace's
|
|
4
|
+
// repos. The match is a PORTABLE case-insensitive SUBSTRING (`lower(col) LIKE pattern ESCAPE '\'`,
|
|
5
|
+
// term lowercased in JS) so the SAME query runs on SQLite (local) and Postgres (cloud) — no dialect
|
|
6
|
+
// fork, no FTS operator. Substring is the right semantics for the user's goal ("pinpoint where
|
|
7
|
+
// certain text exists"). Every query filters on the denormalized `accountId`, so cross-account
|
|
8
|
+
// isolation is a single indexed predicate. A Postgres `pg_trgm` GIN index (see schema.pg.ts) is a
|
|
9
|
+
// drop-in accelerator.
|
|
10
|
+
//
|
|
11
|
+
// ⚠ NOTHING HERE PERSISTS A SCOPE. `search_index` stores `accountId` + `repoId` and the scope is
|
|
12
|
+
// applied at QUERY time from the caller's resolved repo list, so the workspace refactor needs no
|
|
13
|
+
// data migration on this table — unlike the plugin's report caches, which persist a `scope_key`.
|
|
14
|
+
//
|
|
15
|
+
// Case-folding caveat: SQL `lower()` folds ASCII on both dialects, but only Postgres folds non-ASCII
|
|
16
|
+
// (accents, Cyrillic, …) — better-sqlite3 has no ICU. So a lowercase-accented query for
|
|
17
|
+
// upper-accented content ("café" ⇢ "CAFÉ") matches on cloud but not locally. Acceptable: the corpus
|
|
18
|
+
// (code, PR text) is ASCII-dominant, and closing it would need a normalized column + migration for a
|
|
19
|
+
// rare case. ASCII search — the overwhelming majority — is identical on both.
|
|
20
|
+
const { searchIndex, pullRequests, repos, users } = schema;
|
|
21
|
+
const { reviews: reviewsT, reviewComments: rcT, prComments: pcT } = schema;
|
|
22
|
+
const MAX_TERMS = 6;
|
|
23
|
+
const SNIPPET_RADIUS = 90;
|
|
24
|
+
const asDate = (d) => d instanceof Date ? d : new Date(d == null ? 0 : Number(d) * (Number(d) > 1e12 ? 1 : 1000));
|
|
25
|
+
// Collapse whitespace + hard-cap the indexed text (shared with the persist path in upsert.ts). A
|
|
26
|
+
// review/description can be arbitrarily long; the index only needs enough to match + snippet.
|
|
27
|
+
export const SEARCH_TEXT_MAX = 4000;
|
|
28
|
+
export function searchText(...parts) {
|
|
29
|
+
const joined = parts
|
|
30
|
+
.map((p) => (p ?? '').trim())
|
|
31
|
+
.filter((p) => p.length > 0)
|
|
32
|
+
.join('\n')
|
|
33
|
+
.replace(/\s+/g, ' ')
|
|
34
|
+
.trim();
|
|
35
|
+
return joined.length > SEARCH_TEXT_MAX ? joined.slice(0, SEARCH_TEXT_MAX) : joined;
|
|
36
|
+
}
|
|
37
|
+
// Split a raw query into up to MAX_TERMS lowercased terms; a "quoted phrase" is one term. The
|
|
38
|
+
// terms are ANDed for hits (all must appear) and ORed for people (any name fragment matches).
|
|
39
|
+
function parseTerms(query) {
|
|
40
|
+
const terms = [];
|
|
41
|
+
const re = /"([^"]+)"|(\S+)/g;
|
|
42
|
+
let m;
|
|
43
|
+
while ((m = re.exec(query)) != null && terms.length < MAX_TERMS) {
|
|
44
|
+
const t = (m[1] ?? m[2] ?? '').trim().toLowerCase();
|
|
45
|
+
if (t)
|
|
46
|
+
terms.push(t);
|
|
47
|
+
}
|
|
48
|
+
return terms;
|
|
49
|
+
}
|
|
50
|
+
// A `%term%` LIKE pattern with the LIKE metacharacters (%, _, \) escaped so the term matches
|
|
51
|
+
// literally (paired with `ESCAPE '\'` in likeCol).
|
|
52
|
+
function likePattern(term) {
|
|
53
|
+
return `%${term.replace(/([\\%_])/g, '\\$1')}%`;
|
|
54
|
+
}
|
|
55
|
+
// `lower(col) LIKE pattern ESCAPE '\'` — the portable substring predicate. `pattern` is bound as a
|
|
56
|
+
// parameter (no injection); the ESCAPE clause is literal SQL identical on both dialects.
|
|
57
|
+
function likeCol(col, pattern) {
|
|
58
|
+
return sql `lower(${col}) like ${pattern} escape '\\'`;
|
|
59
|
+
}
|
|
60
|
+
// A short excerpt of the matched text, centred on the earliest matched term (so the reader sees WHY
|
|
61
|
+
// it matched). Falls back to the head of the text when only the author matched.
|
|
62
|
+
function makeSnippet(body, terms) {
|
|
63
|
+
const lower = body.toLowerCase();
|
|
64
|
+
let idx = -1;
|
|
65
|
+
for (const t of terms) {
|
|
66
|
+
const i = lower.indexOf(t);
|
|
67
|
+
if (i >= 0 && (idx < 0 || i < idx))
|
|
68
|
+
idx = i;
|
|
69
|
+
}
|
|
70
|
+
if (idx < 0) {
|
|
71
|
+
return body.length > SNIPPET_RADIUS * 2 ? `${body.slice(0, SNIPPET_RADIUS * 2)}…` : body;
|
|
72
|
+
}
|
|
73
|
+
const start = Math.max(0, idx - SNIPPET_RADIUS);
|
|
74
|
+
const end = Math.min(body.length, idx + SNIPPET_RADIUS);
|
|
75
|
+
return `${start > 0 ? '…' : ''}${body.slice(start, end).trim()}${end < body.length ? '…' : ''}`;
|
|
76
|
+
}
|
|
77
|
+
const toIso = (d) => {
|
|
78
|
+
if (d == null)
|
|
79
|
+
return '';
|
|
80
|
+
const ms = d instanceof Date ? d.getTime() : Number(d) * (Number(d) > 1e12 ? 1 : 1000);
|
|
81
|
+
return new Date(ms).toISOString();
|
|
82
|
+
};
|
|
83
|
+
export async function searchPrs(accountId, opts) {
|
|
84
|
+
const query = opts.query.trim();
|
|
85
|
+
const terms = parseTerms(query);
|
|
86
|
+
// No terms, or an empty workspace → nothing to match.
|
|
87
|
+
if (terms.length === 0 || opts.repoIds.length === 0) {
|
|
88
|
+
return { query, hits: [], people: [], total: 0 };
|
|
89
|
+
}
|
|
90
|
+
// Each term must match the body OR the author's login/display name (ANDed across terms).
|
|
91
|
+
const termMatch = (term) => {
|
|
92
|
+
const pat = likePattern(term);
|
|
93
|
+
return or(likeCol(searchIndex.body, pat), likeCol(users.githubLogin, pat), likeCol(users.displayName, pat));
|
|
94
|
+
};
|
|
95
|
+
const repoFilter = inArray(searchIndex.repoId, opts.repoIds);
|
|
96
|
+
const kindFilter = opts.kinds && opts.kinds.length > 0 ? inArray(searchIndex.kind, opts.kinds) : undefined;
|
|
97
|
+
const where = and(eq(searchIndex.accountId, accountId), repoFilter, kindFilter, ...terms.map(termMatch));
|
|
98
|
+
const [rows, totalRows, people] = await Promise.all([
|
|
99
|
+
db
|
|
100
|
+
.select({
|
|
101
|
+
kind: searchIndex.kind,
|
|
102
|
+
prId: searchIndex.prId,
|
|
103
|
+
refId: searchIndex.refId,
|
|
104
|
+
threadId: searchIndex.threadId,
|
|
105
|
+
authorId: searchIndex.authorId,
|
|
106
|
+
body: searchIndex.body,
|
|
107
|
+
createdAt: searchIndex.createdAt,
|
|
108
|
+
prNumber: pullRequests.number,
|
|
109
|
+
prTitle: pullRequests.title,
|
|
110
|
+
prState: pullRequests.state,
|
|
111
|
+
repoId: repos.id,
|
|
112
|
+
repoOwner: repos.owner,
|
|
113
|
+
repoName: repos.name,
|
|
114
|
+
authorLogin: users.githubLogin,
|
|
115
|
+
authorAvatar: users.avatarUrl,
|
|
116
|
+
})
|
|
117
|
+
.from(searchIndex)
|
|
118
|
+
.innerJoin(pullRequests, eq(searchIndex.prId, pullRequests.id))
|
|
119
|
+
.innerJoin(repos, eq(searchIndex.repoId, repos.id))
|
|
120
|
+
.leftJoin(users, eq(searchIndex.authorId, users.id))
|
|
121
|
+
.where(where)
|
|
122
|
+
.orderBy(desc(searchIndex.createdAt))
|
|
123
|
+
.limit(opts.limit)
|
|
124
|
+
.offset(opts.offset)
|
|
125
|
+
.execute(),
|
|
126
|
+
db
|
|
127
|
+
.select({ n: sql `count(*)` })
|
|
128
|
+
// Only the users join is needed for the WHERE (author match); pullRequests/repos joins are
|
|
129
|
+
// display-only, so the count skips them.
|
|
130
|
+
.from(searchIndex)
|
|
131
|
+
.leftJoin(users, eq(searchIndex.authorId, users.id))
|
|
132
|
+
.where(where)
|
|
133
|
+
.execute(),
|
|
134
|
+
findPeople(accountId, opts.repoIds, terms),
|
|
135
|
+
]);
|
|
136
|
+
const hits = rows.map((r) => ({
|
|
137
|
+
kind: r.kind,
|
|
138
|
+
prId: r.prId,
|
|
139
|
+
prNumber: r.prNumber,
|
|
140
|
+
prTitle: r.prTitle,
|
|
141
|
+
prState: r.prState,
|
|
142
|
+
repoId: r.repoId,
|
|
143
|
+
repoFullName: `${r.repoOwner}/${r.repoName}`,
|
|
144
|
+
refId: r.refId,
|
|
145
|
+
threadId: r.threadId,
|
|
146
|
+
authorId: r.authorId,
|
|
147
|
+
authorLogin: r.authorLogin ?? null,
|
|
148
|
+
authorAvatarUrl: r.authorAvatar ?? null,
|
|
149
|
+
snippet: makeSnippet(r.body, terms),
|
|
150
|
+
createdAt: toIso(r.createdAt),
|
|
151
|
+
}));
|
|
152
|
+
return { query, hits, people, total: Number(totalRows[0]?.n ?? 0) };
|
|
153
|
+
}
|
|
154
|
+
// The "People" facet: authors whose login/display name matches ANY term AND who have indexed
|
|
155
|
+
// activity in the searched repos (so the person is real + relevant), ranked by how much they've
|
|
156
|
+
// authored. `repoIds` is the same concrete workspace-bounded list `searchPrs` matched on — the
|
|
157
|
+
// caller has already returned empty for `[]`, so the facet can never widen past the hits.
|
|
158
|
+
async function findPeople(accountId, repoIds, terms) {
|
|
159
|
+
const nameMatch = or(...terms.map((t) => {
|
|
160
|
+
const pat = likePattern(t);
|
|
161
|
+
return or(likeCol(users.githubLogin, pat), likeCol(users.displayName, pat));
|
|
162
|
+
}));
|
|
163
|
+
const repoFilter = inArray(searchIndex.repoId, repoIds);
|
|
164
|
+
const n = sql `count(*)`;
|
|
165
|
+
const rows = await db
|
|
166
|
+
.select({
|
|
167
|
+
id: users.id,
|
|
168
|
+
login: users.githubLogin,
|
|
169
|
+
displayName: users.displayName,
|
|
170
|
+
avatarUrl: users.avatarUrl,
|
|
171
|
+
n,
|
|
172
|
+
})
|
|
173
|
+
.from(searchIndex)
|
|
174
|
+
.innerJoin(users, eq(searchIndex.authorId, users.id))
|
|
175
|
+
.where(and(eq(searchIndex.accountId, accountId), repoFilter, nameMatch))
|
|
176
|
+
.groupBy(users.id, users.githubLogin, users.displayName, users.avatarUrl)
|
|
177
|
+
.orderBy(desc(n))
|
|
178
|
+
.limit(10)
|
|
179
|
+
.execute();
|
|
180
|
+
return rows.map((r) => ({
|
|
181
|
+
id: r.id,
|
|
182
|
+
login: r.login,
|
|
183
|
+
displayName: r.displayName ?? null,
|
|
184
|
+
avatarUrl: r.avatarUrl ?? null,
|
|
185
|
+
matchCount: Number(r.n),
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
188
|
+
// ---- backfill (startup) ------------------------------------------------------------------------
|
|
189
|
+
// Index PRs that predate the search feature from ALREADY-STORED data (title + review/comment
|
|
190
|
+
// bodies; the description only when pullRequests.body was persisted, i.e. non-lean — lean PRs get
|
|
191
|
+
// their description indexed on the next sync via bodyText). Idempotent: only touches PRs that have
|
|
192
|
+
// no search rows yet, in bounded batches, so a second run after a full pass is a cheap NOT-EXISTS
|
|
193
|
+
// scan. Safe to fire-and-forget at startup.
|
|
194
|
+
export async function backfillSearchIndex(batchSize = 200, maxBatches = 2000) {
|
|
195
|
+
let indexed = 0;
|
|
196
|
+
for (let b = 0; b < maxBatches; b++) {
|
|
197
|
+
const prRows = (await db
|
|
198
|
+
.select({
|
|
199
|
+
id: pullRequests.id,
|
|
200
|
+
accountId: pullRequests.accountId,
|
|
201
|
+
repoId: pullRequests.repoId,
|
|
202
|
+
title: pullRequests.title,
|
|
203
|
+
body: pullRequests.body,
|
|
204
|
+
authorId: pullRequests.authorId,
|
|
205
|
+
openedAt: pullRequests.openedAt,
|
|
206
|
+
})
|
|
207
|
+
.from(pullRequests)
|
|
208
|
+
.where(sql `not exists (select 1 from ${searchIndex} where ${searchIndex.prId} = ${pullRequests.id})`)
|
|
209
|
+
.limit(batchSize)
|
|
210
|
+
.execute());
|
|
211
|
+
if (prRows.length === 0)
|
|
212
|
+
break;
|
|
213
|
+
const prIds = prRows.map((p) => p.id);
|
|
214
|
+
const acctOf = new Map(prRows.map((p) => [p.id, p.accountId]));
|
|
215
|
+
const repoOf = new Map(prRows.map((p) => [p.id, p.repoId]));
|
|
216
|
+
const [revRows, rcRows, pcRows] = await Promise.all([
|
|
217
|
+
db
|
|
218
|
+
.select({ id: reviewsT.id, prId: reviewsT.prId, authorId: reviewsT.authorId, body: reviewsT.body, at: reviewsT.submittedAt })
|
|
219
|
+
.from(reviewsT)
|
|
220
|
+
.where(inArray(reviewsT.prId, prIds))
|
|
221
|
+
.execute(),
|
|
222
|
+
db
|
|
223
|
+
.select({ id: rcT.id, prId: rcT.prId, threadId: rcT.threadId, authorId: rcT.authorId, body: rcT.body, at: rcT.createdAt })
|
|
224
|
+
.from(rcT)
|
|
225
|
+
.where(inArray(rcT.prId, prIds))
|
|
226
|
+
.execute(),
|
|
227
|
+
db
|
|
228
|
+
.select({ id: pcT.id, prId: pcT.prId, authorId: pcT.authorId, body: pcT.body, at: pcT.createdAt })
|
|
229
|
+
.from(pcT)
|
|
230
|
+
.where(inArray(pcT.prId, prIds))
|
|
231
|
+
.execute(),
|
|
232
|
+
]);
|
|
233
|
+
const rows = [];
|
|
234
|
+
for (const p of prRows) {
|
|
235
|
+
const text = searchText(p.title, p.body);
|
|
236
|
+
if (text)
|
|
237
|
+
rows.push({
|
|
238
|
+
accountId: p.accountId,
|
|
239
|
+
repoId: p.repoId,
|
|
240
|
+
prId: p.id,
|
|
241
|
+
kind: 'pr',
|
|
242
|
+
refId: p.id,
|
|
243
|
+
threadId: null,
|
|
244
|
+
authorId: p.authorId,
|
|
245
|
+
body: text,
|
|
246
|
+
createdAt: asDate(p.openedAt),
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
for (const r of revRows) {
|
|
250
|
+
const text = searchText(r.body);
|
|
251
|
+
if (text)
|
|
252
|
+
rows.push({
|
|
253
|
+
accountId: acctOf.get(r.prId),
|
|
254
|
+
repoId: repoOf.get(r.prId),
|
|
255
|
+
prId: r.prId,
|
|
256
|
+
kind: 'review',
|
|
257
|
+
refId: r.id,
|
|
258
|
+
threadId: null,
|
|
259
|
+
authorId: r.authorId,
|
|
260
|
+
body: text,
|
|
261
|
+
createdAt: asDate(r.at),
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
for (const c of rcRows) {
|
|
265
|
+
const text = searchText(c.body);
|
|
266
|
+
if (text)
|
|
267
|
+
rows.push({
|
|
268
|
+
accountId: acctOf.get(c.prId),
|
|
269
|
+
repoId: repoOf.get(c.prId),
|
|
270
|
+
prId: c.prId,
|
|
271
|
+
kind: 'review_comment',
|
|
272
|
+
refId: c.id,
|
|
273
|
+
threadId: c.threadId,
|
|
274
|
+
authorId: c.authorId,
|
|
275
|
+
body: text,
|
|
276
|
+
createdAt: asDate(c.at),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
for (const c of pcRows) {
|
|
280
|
+
const text = searchText(c.body);
|
|
281
|
+
if (text)
|
|
282
|
+
rows.push({
|
|
283
|
+
accountId: acctOf.get(c.prId),
|
|
284
|
+
repoId: repoOf.get(c.prId),
|
|
285
|
+
prId: c.prId,
|
|
286
|
+
kind: 'pr_comment',
|
|
287
|
+
refId: c.id,
|
|
288
|
+
threadId: null,
|
|
289
|
+
authorId: c.authorId,
|
|
290
|
+
body: text,
|
|
291
|
+
createdAt: asDate(c.at),
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
// One transaction per batch. A concurrent live persistPr may have populated some of these PRs
|
|
295
|
+
// (with the richer, lean-safe rows from bodyText) BETWEEN this batch's un-indexed SELECT and
|
|
296
|
+
// here. Re-check inside the tx and INSERT only for PRs that STILL have no rows — never delete —
|
|
297
|
+
// so the backfill can neither clobber those fresh rows with its staler snapshot nor duplicate
|
|
298
|
+
// them. Chunked under SQLite's bound-variable limit.
|
|
299
|
+
await runTransaction(async (tx) => {
|
|
300
|
+
const already = (await tx
|
|
301
|
+
.select({ prId: searchIndex.prId })
|
|
302
|
+
.from(searchIndex)
|
|
303
|
+
.where(inArray(searchIndex.prId, prIds))
|
|
304
|
+
.execute());
|
|
305
|
+
const populated = new Set(already.map((r) => r.prId));
|
|
306
|
+
const fresh = rows.filter((r) => !populated.has(r.prId));
|
|
307
|
+
for (let i = 0; i < fresh.length; i += 100) {
|
|
308
|
+
await tx.insert(searchIndex).values(fresh.slice(i, i + 100)).execute();
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
indexed += prRows.length;
|
|
312
|
+
if (prRows.length < batchSize)
|
|
313
|
+
break; // last partial batch
|
|
314
|
+
}
|
|
315
|
+
return indexed;
|
|
316
|
+
}
|
|
317
|
+
//# sourceMappingURL=search.js.map
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// ── LIVE AUTOMATION SECURITY ALERTS, DERIVED ON READ (CORE, deterministic, no AI) ──────────────
|
|
2
|
+
//
|
|
3
|
+
// The Dependencies tab's second source of `security` cards: a security TOOL's own comment on a PR
|
|
4
|
+
// naming a KNOWN ADVISORY (a CVE / GHSA / … id) — Socket's "Critical CVE" report, a Dependency
|
|
5
|
+
// Review failure, an AI reviewer's own thread finding. Nothing is stored: every read re-derives the
|
|
6
|
+
// alert from the comment rows the sync already persists (comment and review bodies are ALWAYS
|
|
7
|
+
// persisted), so an alert clears itself by STATE — the tool's latest comment stops naming one, or
|
|
8
|
+
// the review thread is resolved / likely addressed — with no tombstone to forget.
|
|
9
|
+
//
|
|
10
|
+
// The rules are the detector's (sync/security-detect.ts `evaluateSecurityAlerts`); this file only
|
|
11
|
+
// reads the candidate rows. ⚠ The SQL pre-filter is built straight from `SECURITY_ALERT_PREFILTER`
|
|
12
|
+
// and may be LOOSER than the evaluator, never stricter: a tool's "all clear" (Socket's "All alerts
|
|
13
|
+
// resolved") names no advisory, and it must still reach the evaluator, because it is the latest row
|
|
14
|
+
// that decides. SQLite's case-blind `LIKE` and an `_` in a literal both widen it; the evaluator
|
|
15
|
+
// re-applies every literal exact-case, so both dialects alert on the same rows.
|
|
16
|
+
//
|
|
17
|
+
// ⚠ COMMENTS AND REVIEWS ARE NARROWED TO THE TOOLS' OWN ACCOUNTS, and are still exactly as loose:
|
|
18
|
+
// on those two surfaces an author-gated rule reads only its own tool's login, an author-free rule
|
|
19
|
+
// (Frogbot, Checkmarx — they post through github-actions or a person's token) reads anyone's row
|
|
20
|
+
// that carries its marker, and the `reviewer` fallback never reads them at all. THREADS are not
|
|
21
|
+
// narrowed: that fallback reads every automated root carrying any literal, which on Postgres (a
|
|
22
|
+
// case-sensitive `LIKE`) no narrower SQL can promise to keep. COST, measured on a copy of the dev DB
|
|
23
|
+
// over Erxes' 83 active PRs, whose 5.2 MB of text is 99.8% automation's own (CodeRabbit and co.):
|
|
24
|
+
// all 18 literals over every comment and review took ~65 ms of a ~400 ms fold; the narrowed reads
|
|
25
|
+
// take ~10 ms. Threads stay at ~60 ms.
|
|
26
|
+
import { and, asc, eq, inArray, like, ne, or, sql } from 'drizzle-orm';
|
|
27
|
+
import { normalizeBotLogin } from '../shared/index.js';
|
|
28
|
+
import { db, schema } from './client.js';
|
|
29
|
+
import { evaluateSecurityAlerts, SECURITY_ALERT_PREFILTER, SECURITY_ALERT_RULES, } from '../sync/security-detect.js';
|
|
30
|
+
const { prComments, reviews, reviewComments, reviewThreads, users } = schema;
|
|
31
|
+
/** Each AUTHOR-FREE rule's marker (`author: null`): a `SECURITY_ALERT_PREFILTER` literal that every
|
|
32
|
+
* body the rule `identifies` contains, so a person's row carrying it still reaches the evaluator.
|
|
33
|
+
* pending-deps.test.ts holds this to the rules. */
|
|
34
|
+
export const AUTHOR_FREE_ALERT_MARKERS = {
|
|
35
|
+
frogbot: 'Frogbot',
|
|
36
|
+
checkmarx: 'Checkmarx One',
|
|
37
|
+
};
|
|
38
|
+
/** `body LIKE '%s1%' OR body LIKE '%s2%' OR …` over the given literals. */
|
|
39
|
+
function anyLike(body, literals) {
|
|
40
|
+
return or(...literals.map((s) => like(body, `%${s}%`)));
|
|
41
|
+
}
|
|
42
|
+
/** The rows of a COMMENT or REVIEW surface that may reach the evaluator — every row a rule reading
|
|
43
|
+
* that surface could use. `null` = no rule reads it from anyone here: skip the read.
|
|
44
|
+
* `toolIds` = the automated authors whose login an author-gated rule on this surface names. */
|
|
45
|
+
function surfaceCandidates(surface, authorId, body, toolIds) {
|
|
46
|
+
const parts = [];
|
|
47
|
+
if (toolIds.length > 0) {
|
|
48
|
+
parts.push(and(inArray(authorId, [...toolIds]), anyLike(body, SECURITY_ALERT_PREFILTER)));
|
|
49
|
+
}
|
|
50
|
+
for (const rule of SECURITY_ALERT_RULES) {
|
|
51
|
+
if (rule.author !== null || !rule.surfaces.includes(surface))
|
|
52
|
+
continue;
|
|
53
|
+
const marker = AUTHOR_FREE_ALERT_MARKERS[rule.source];
|
|
54
|
+
// An author-free rule with no marker here reads the whole pre-filter, from anyone.
|
|
55
|
+
parts.push(marker != null ? like(body, `%${marker}%`) : anyLike(body, SECURITY_ALERT_PREFILTER));
|
|
56
|
+
}
|
|
57
|
+
return parts.length > 0 ? or(...parts) : null;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Live automation ALERTS naming a known advisory, for the given open PRs. DB-only, no GitHub call.
|
|
61
|
+
* Every live alert per PR, newest first, uncapped here — the card builder slices what it shows and
|
|
62
|
+
* carries the full count beside it.
|
|
63
|
+
*
|
|
64
|
+
* ⚠ TENANCY: `pr_comments` / `reviews` / `review_comments` carry no account_id. Every read here is
|
|
65
|
+
* keyed by `pr_id IN (prIds)`, and the ONLY caller passes the fold's account-scoped `openPrIds`.
|
|
66
|
+
* Never export a variant that takes ids from a request.
|
|
67
|
+
*/
|
|
68
|
+
export async function deriveSecurityAlerts(prIds, automatedIds, kindOf) {
|
|
69
|
+
if (prIds.length === 0)
|
|
70
|
+
return new Map();
|
|
71
|
+
const ids = [...prIds];
|
|
72
|
+
const threadBody = sql `coalesce(${reviewComments.body}, ${reviewComments.excerpt})`;
|
|
73
|
+
// The tools' own accounts among the automated authors. `users` is GLOBAL — read by id only.
|
|
74
|
+
const automatedLogins = automatedIds.size > 0
|
|
75
|
+
? await db
|
|
76
|
+
.select({ id: users.id, login: users.githubLogin })
|
|
77
|
+
.from(users)
|
|
78
|
+
.where(inArray(users.id, [...automatedIds]))
|
|
79
|
+
.execute()
|
|
80
|
+
: [];
|
|
81
|
+
const toolIdsFor = (surface) => automatedLogins
|
|
82
|
+
.filter((u) => SECURITY_ALERT_RULES.some((r) => r.author !== null && r.surfaces.includes(surface) && r.author(normalizeBotLogin(u.login))))
|
|
83
|
+
.map((u) => u.id);
|
|
84
|
+
const commentWhere = surfaceCandidates('comment', prComments.authorId, prComments.body, toolIdsFor('comment'));
|
|
85
|
+
const reviewWhere = surfaceCandidates('review', reviews.authorId, reviews.body, toolIdsFor('review'));
|
|
86
|
+
const [commentRows, reviewRows, threadRows] = await Promise.all([
|
|
87
|
+
commentWhere == null
|
|
88
|
+
? []
|
|
89
|
+
: db
|
|
90
|
+
.select({
|
|
91
|
+
id: prComments.id,
|
|
92
|
+
prId: prComments.prId,
|
|
93
|
+
authorId: prComments.authorId,
|
|
94
|
+
body: prComments.body,
|
|
95
|
+
createdAt: prComments.createdAt,
|
|
96
|
+
})
|
|
97
|
+
.from(prComments)
|
|
98
|
+
.where(and(inArray(prComments.prId, ids), commentWhere))
|
|
99
|
+
.execute(),
|
|
100
|
+
reviewWhere == null
|
|
101
|
+
? []
|
|
102
|
+
: db
|
|
103
|
+
.select({
|
|
104
|
+
id: reviews.id,
|
|
105
|
+
prId: reviews.prId,
|
|
106
|
+
authorId: reviews.authorId,
|
|
107
|
+
body: reviews.body,
|
|
108
|
+
createdAt: reviews.submittedAt,
|
|
109
|
+
})
|
|
110
|
+
.from(reviews)
|
|
111
|
+
// A 'pending' review is an unsubmitted draft — it has said nothing yet.
|
|
112
|
+
.where(and(inArray(reviews.prId, ids), ne(reviews.state, 'pending'), reviewWhere))
|
|
113
|
+
.execute(),
|
|
114
|
+
db
|
|
115
|
+
.select({
|
|
116
|
+
id: reviewComments.id,
|
|
117
|
+
prId: reviewComments.prId,
|
|
118
|
+
authorId: reviewComments.authorId,
|
|
119
|
+
body: threadBody,
|
|
120
|
+
createdAt: reviewComments.createdAt,
|
|
121
|
+
threadId: reviewComments.threadId,
|
|
122
|
+
isResolved: reviewThreads.isResolved,
|
|
123
|
+
derivedState: reviewThreads.derivedState,
|
|
124
|
+
})
|
|
125
|
+
.from(reviewComments)
|
|
126
|
+
.innerJoin(reviewThreads, eq(reviewThreads.id, reviewComments.threadId))
|
|
127
|
+
.where(and(inArray(reviewComments.prId, ids), anyLike(threadBody, SECURITY_ALERT_PREFILTER)))
|
|
128
|
+
.execute(),
|
|
129
|
+
]);
|
|
130
|
+
// WHICH candidate is its thread's ROOT — the earliest (created_at, id) of the WHOLE thread, not
|
|
131
|
+
// of the candidates (a reply can pass the pre-filter while the root does not). One extra read over
|
|
132
|
+
// the candidate threads, on `rc_thread_idx`; no correlated subquery, so it stays portable.
|
|
133
|
+
const threadIds = [...new Set(threadRows.map((r) => r.threadId))];
|
|
134
|
+
const rootIdOf = new Map();
|
|
135
|
+
if (threadIds.length > 0) {
|
|
136
|
+
const all = await db
|
|
137
|
+
.select({ id: reviewComments.id, threadId: reviewComments.threadId })
|
|
138
|
+
.from(reviewComments)
|
|
139
|
+
.where(inArray(reviewComments.threadId, threadIds))
|
|
140
|
+
.orderBy(asc(reviewComments.createdAt), asc(reviewComments.id))
|
|
141
|
+
.execute();
|
|
142
|
+
// Ordered (created_at, id), so the first row seen per thread is its root.
|
|
143
|
+
for (const r of all)
|
|
144
|
+
if (!rootIdOf.has(r.threadId))
|
|
145
|
+
rootIdOf.set(r.threadId, r.id);
|
|
146
|
+
}
|
|
147
|
+
// Logins, for the rules that name their tool's account. `users` is GLOBAL — read by id only.
|
|
148
|
+
const authorIds = [
|
|
149
|
+
...new Set([...commentRows, ...reviewRows, ...threadRows]
|
|
150
|
+
.map((r) => r.authorId)
|
|
151
|
+
.filter((id) => id != null)),
|
|
152
|
+
];
|
|
153
|
+
const loginOf = new Map();
|
|
154
|
+
if (authorIds.length > 0) {
|
|
155
|
+
for (const u of await db
|
|
156
|
+
.select({ id: users.id, login: users.githubLogin })
|
|
157
|
+
.from(users)
|
|
158
|
+
.where(inArray(users.id, authorIds))
|
|
159
|
+
.execute())
|
|
160
|
+
loginOf.set(u.id, u.login);
|
|
161
|
+
}
|
|
162
|
+
const login = (id) => (id != null ? loginOf.get(id) ?? null : null);
|
|
163
|
+
const rows = [
|
|
164
|
+
...commentRows.map((r) => ({
|
|
165
|
+
surface: 'comment',
|
|
166
|
+
rowId: r.id,
|
|
167
|
+
prId: r.prId,
|
|
168
|
+
authorId: r.authorId,
|
|
169
|
+
authorLogin: login(r.authorId),
|
|
170
|
+
body: r.body ?? '',
|
|
171
|
+
createdAt: r.createdAt,
|
|
172
|
+
threadId: null,
|
|
173
|
+
isRoot: false,
|
|
174
|
+
threadResolved: false,
|
|
175
|
+
threadState: null,
|
|
176
|
+
})),
|
|
177
|
+
...reviewRows.map((r) => ({
|
|
178
|
+
surface: 'review',
|
|
179
|
+
rowId: r.id,
|
|
180
|
+
prId: r.prId,
|
|
181
|
+
authorId: r.authorId,
|
|
182
|
+
authorLogin: login(r.authorId),
|
|
183
|
+
body: r.body ?? '',
|
|
184
|
+
createdAt: r.createdAt,
|
|
185
|
+
threadId: null,
|
|
186
|
+
isRoot: false,
|
|
187
|
+
threadResolved: false,
|
|
188
|
+
threadState: null,
|
|
189
|
+
})),
|
|
190
|
+
...threadRows.map((r) => ({
|
|
191
|
+
surface: 'thread',
|
|
192
|
+
rowId: r.id,
|
|
193
|
+
prId: r.prId,
|
|
194
|
+
authorId: r.authorId,
|
|
195
|
+
authorLogin: login(r.authorId),
|
|
196
|
+
body: r.body ?? '',
|
|
197
|
+
createdAt: r.createdAt,
|
|
198
|
+
threadId: r.threadId,
|
|
199
|
+
isRoot: rootIdOf.get(r.threadId) === r.id,
|
|
200
|
+
threadResolved: r.isResolved,
|
|
201
|
+
threadState: r.derivedState,
|
|
202
|
+
})),
|
|
203
|
+
];
|
|
204
|
+
const out = new Map();
|
|
205
|
+
for (const [prId, alerts] of evaluateSecurityAlerts(rows, automatedIds)) {
|
|
206
|
+
out.set(prId, alerts.map((a) => ({
|
|
207
|
+
source: a.source,
|
|
208
|
+
authorId: a.authorId,
|
|
209
|
+
vendorKind: kindOf.get(a.authorId) ?? null,
|
|
210
|
+
surface: a.surface,
|
|
211
|
+
threadId: a.threadId,
|
|
212
|
+
advisoryIds: a.advisoryIds,
|
|
213
|
+
at: a.at.toISOString(),
|
|
214
|
+
})));
|
|
215
|
+
}
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
//# sourceMappingURL=security-alerts.js.map
|