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,482 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { eq } from 'drizzle-orm';
|
|
3
|
+
import { compactMyTurnSettings } from '../shared/index.js';
|
|
4
|
+
import { db, schema } from '../db/client.js';
|
|
5
|
+
import { getGithubTokenAsync } from '../github/auth.js';
|
|
6
|
+
import { decryptToken } from './crypto.js';
|
|
7
|
+
const STALE_MS = 24 * 60 * 60 * 1000; // re-fetch the local identity once a day
|
|
8
|
+
// The synthesized local account is always id 1 (seeded by migration 0008).
|
|
9
|
+
export const LOCAL_ACCOUNT_ID = 1;
|
|
10
|
+
function fetchFromGh() {
|
|
11
|
+
try {
|
|
12
|
+
const out = execFileSync('gh', ['api', 'user'], { encoding: 'utf-8' });
|
|
13
|
+
const parsed = JSON.parse(out);
|
|
14
|
+
if (!parsed.login || !parsed.node_id)
|
|
15
|
+
return null;
|
|
16
|
+
return {
|
|
17
|
+
login: parsed.login,
|
|
18
|
+
node_id: parsed.node_id,
|
|
19
|
+
name: parsed.name ?? null,
|
|
20
|
+
avatar_url: parsed.avatar_url ?? null,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// gh missing / not authed / offline — non-fatal; my-turn just stays empty.
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function rowToAccount(row) {
|
|
29
|
+
return {
|
|
30
|
+
id: row.id,
|
|
31
|
+
githubUserId: row.githubUserId,
|
|
32
|
+
githubLogin: row.githubLogin,
|
|
33
|
+
displayName: row.displayName,
|
|
34
|
+
avatarUrl: row.avatarUrl,
|
|
35
|
+
isLocal: row.isLocal,
|
|
36
|
+
plan: row.plan === 'pro' ? 'pro' : 'free',
|
|
37
|
+
stripeCustomerId: row.stripeCustomerId,
|
|
38
|
+
aiCreditAllowance: row.aiCreditAllowance ?? null,
|
|
39
|
+
benchmarkOptIn: row.benchmarkOptIn ?? false,
|
|
40
|
+
largePrCodeLocThreshold: row.largePrCodeLocThreshold ?? null,
|
|
41
|
+
blastRadiusConfig: row.blastRadiusConfig ?? null,
|
|
42
|
+
myTurnSettings: row.myTurnSettings ?? null,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
// Module cache of the local account, set by ensureLocalAccount() at startup so
|
|
46
|
+
// the per-request auth hook can resolve it synchronously.
|
|
47
|
+
let cachedLocalAccount = null;
|
|
48
|
+
/**
|
|
49
|
+
* Guarantee the account's DEFAULT WORKSPACE exists before the account is handed to anyone.
|
|
50
|
+
*
|
|
51
|
+
* A workspace id is the only scope this app has: every scoped read resolves one, repos land in
|
|
52
|
+
* the Default on sync, and reviewer rows key on it. An account with no workspace therefore has
|
|
53
|
+
* no reachable state at all — not an empty board but a resolver with nothing to return — so both
|
|
54
|
+
* account-creation paths below close the gap at the moment the row is created. Migration 0044
|
|
55
|
+
* backfills a Default for every account that existed when it ran; this covers every account
|
|
56
|
+
* created afterwards, which in cloud is all of them.
|
|
57
|
+
*
|
|
58
|
+
* `db/queries.ts` STATICALLY imports `getAccountUserId` from this file, so the import here is
|
|
59
|
+
* DYNAMIC to avoid closing that cycle — the same shape `setBenchmarkConsent` below already uses
|
|
60
|
+
* for the same reason. It costs nothing: both callers are cold (process startup / an OAuth
|
|
61
|
+
* callback) and the module is cached after the first call.
|
|
62
|
+
*
|
|
63
|
+
* It is deliberately NOT swallowed. `ensureDefaultWorkspace` only throws when the partial unique
|
|
64
|
+
* index `workspaces_one_default` is missing, i.e. migration 0044 never ran — and both callers run
|
|
65
|
+
* `runMigrations()` first. Failing there names the real fault; swallowing it would instead have
|
|
66
|
+
* every later request resolve a scope that does not exist.
|
|
67
|
+
*/
|
|
68
|
+
async function ensureAccountWorkspace(accountId) {
|
|
69
|
+
const { ensureDefaultWorkspace } = await import('../db/queries.js');
|
|
70
|
+
await ensureDefaultWorkspace(accountId);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Ensure the synthesized local account (id 1) reflects the locally-authenticated
|
|
74
|
+
* GitHub user, and that it owns a Default workspace. Refetches via `gh api user`
|
|
75
|
+
* on first run and once per day; otherwise returns the cached row.
|
|
76
|
+
*
|
|
77
|
+
* A missing/unauthenticated/offline `gh` is non-fatal — it leaves "you" unknown and
|
|
78
|
+
* triage degrades gracefully to empty. (A failing DB is not: see
|
|
79
|
+
* `ensureAccountWorkspace`.) Local mode only.
|
|
80
|
+
*/
|
|
81
|
+
export async function ensureLocalAccount() {
|
|
82
|
+
const account = await resolveLocalAccount();
|
|
83
|
+
cachedLocalAccount = account;
|
|
84
|
+
// ONE call site covering all three resolution paths — the fresh cache hit included, since an
|
|
85
|
+
// account row that predates the workspace tables reaches this function by that path too.
|
|
86
|
+
if (account)
|
|
87
|
+
await ensureAccountWorkspace(account.id);
|
|
88
|
+
return account;
|
|
89
|
+
}
|
|
90
|
+
/** The `accounts` half of ensureLocalAccount: resolve/refresh the row, no workspace, no caching. */
|
|
91
|
+
async function resolveLocalAccount() {
|
|
92
|
+
const { accounts } = schema;
|
|
93
|
+
const existingRows = await db
|
|
94
|
+
.select()
|
|
95
|
+
.from(accounts)
|
|
96
|
+
.where(eq(accounts.id, LOCAL_ACCOUNT_ID))
|
|
97
|
+
.limit(1)
|
|
98
|
+
.execute();
|
|
99
|
+
const existing = existingRows[0] ?? null;
|
|
100
|
+
const fresh = existing &&
|
|
101
|
+
existing.githubUserId !== '' &&
|
|
102
|
+
existing.lastLoginAt != null &&
|
|
103
|
+
Date.now() - existing.lastLoginAt.getTime() < STALE_MS &&
|
|
104
|
+
// Backfill the display name on the first run after it was added (older rows have
|
|
105
|
+
// it NULL). A genuinely name-less GitHub user re-fetches each startup — cheap;
|
|
106
|
+
// the daily refresh would repopulate it anyway.
|
|
107
|
+
existing.displayName != null;
|
|
108
|
+
if (existing && fresh)
|
|
109
|
+
return rowToAccount(existing);
|
|
110
|
+
const gh = fetchFromGh();
|
|
111
|
+
if (!gh) {
|
|
112
|
+
// Fall back to whatever row exists (the migration seeds a placeholder).
|
|
113
|
+
return existing ? rowToAccount(existing) : null;
|
|
114
|
+
}
|
|
115
|
+
const updatedRows = await db
|
|
116
|
+
.insert(accounts)
|
|
117
|
+
.values({
|
|
118
|
+
id: LOCAL_ACCOUNT_ID,
|
|
119
|
+
githubUserId: gh.node_id,
|
|
120
|
+
githubLogin: gh.login,
|
|
121
|
+
displayName: gh.name,
|
|
122
|
+
avatarUrl: gh.avatar_url,
|
|
123
|
+
isLocal: true,
|
|
124
|
+
lastLoginAt: new Date(),
|
|
125
|
+
})
|
|
126
|
+
.onConflictDoUpdate({
|
|
127
|
+
target: accounts.id,
|
|
128
|
+
set: {
|
|
129
|
+
githubUserId: gh.node_id,
|
|
130
|
+
githubLogin: gh.login,
|
|
131
|
+
displayName: gh.name,
|
|
132
|
+
avatarUrl: gh.avatar_url,
|
|
133
|
+
isLocal: true,
|
|
134
|
+
lastLoginAt: new Date(),
|
|
135
|
+
},
|
|
136
|
+
})
|
|
137
|
+
.returning()
|
|
138
|
+
.execute();
|
|
139
|
+
return updatedRows[0] ? rowToAccount(updatedRows[0]) : null;
|
|
140
|
+
}
|
|
141
|
+
/** The local account from the module cache (no network / no DB). */
|
|
142
|
+
export function getLocalAccountCached() {
|
|
143
|
+
return cachedLocalAccount;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Re-read the LOCAL account's row into the module cache after a column write.
|
|
147
|
+
*
|
|
148
|
+
* ⚠ WITHOUT THIS, A PER-ACCOUNT SETTING SILENTLY DOES NOT SAVE IN LOCAL MODE. The cache is
|
|
149
|
+
* populated once at startup by `ensureLocalAccount` and the per-request hook serves `req.account`
|
|
150
|
+
* straight out of it, so `/api/me` keeps echoing the value the process booted with. The write
|
|
151
|
+
* route's own response is correct, so the SPA paints the new setting — and is then told the old
|
|
152
|
+
* one by the `['me']` refetch its mutation triggers, which reverts the control the user just used.
|
|
153
|
+
* Measured on the shipping large-PR threshold before this existed: POST 900 → 200 OK with
|
|
154
|
+
* `{threshold: 900}`, then `/api/me` → `{threshold: 1500, isDefault: true}`.
|
|
155
|
+
*
|
|
156
|
+
* A no-op in cloud (there is no cache: `registerAccountContext` reads the row per request) and a
|
|
157
|
+
* no-op for any account that is not the cached one.
|
|
158
|
+
*/
|
|
159
|
+
export async function refreshLocalAccountCache(accountId) {
|
|
160
|
+
if (cachedLocalAccount == null || cachedLocalAccount.id !== accountId)
|
|
161
|
+
return;
|
|
162
|
+
const fresh = await getAccountById(accountId);
|
|
163
|
+
if (fresh)
|
|
164
|
+
cachedLocalAccount = fresh;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Upsert a cloud account from a completed OAuth sign-in (keyed on the GitHub
|
|
168
|
+
* user node id). Re-login refreshes the login/avatar, the encrypted token, and
|
|
169
|
+
* lastLoginAt, and guarantees the account's Default workspace. Returns the account.
|
|
170
|
+
*
|
|
171
|
+
* This is the ONLY path that creates a cloud account, so it is the only place a
|
|
172
|
+
* brand-new tenant can be given the one scope the app has — migration 0044's
|
|
173
|
+
* backfill can only reach accounts that already existed when it ran.
|
|
174
|
+
*/
|
|
175
|
+
export async function upsertCloudAccount(input) {
|
|
176
|
+
const { accounts } = schema;
|
|
177
|
+
const now = new Date();
|
|
178
|
+
const rows = await db
|
|
179
|
+
.insert(accounts)
|
|
180
|
+
.values({
|
|
181
|
+
githubUserId: input.githubUserId,
|
|
182
|
+
githubLogin: input.githubLogin,
|
|
183
|
+
displayName: input.displayName,
|
|
184
|
+
avatarUrl: input.avatarUrl,
|
|
185
|
+
accessTokenEnc: input.accessTokenEnc,
|
|
186
|
+
isLocal: false,
|
|
187
|
+
lastLoginAt: now,
|
|
188
|
+
// Seed activity at sign-in so the user's repos are eligible on the very next
|
|
189
|
+
// scheduled sync tick (don't wait for the first heartbeat).
|
|
190
|
+
lastActiveAt: now,
|
|
191
|
+
})
|
|
192
|
+
.onConflictDoUpdate({
|
|
193
|
+
target: accounts.githubUserId,
|
|
194
|
+
set: {
|
|
195
|
+
githubLogin: input.githubLogin,
|
|
196
|
+
displayName: input.displayName,
|
|
197
|
+
avatarUrl: input.avatarUrl,
|
|
198
|
+
accessTokenEnc: input.accessTokenEnc,
|
|
199
|
+
lastLoginAt: now,
|
|
200
|
+
lastActiveAt: now,
|
|
201
|
+
},
|
|
202
|
+
})
|
|
203
|
+
.returning()
|
|
204
|
+
.execute();
|
|
205
|
+
const account = rowToAccount(rows[0]);
|
|
206
|
+
// Before returning: the callback redirects straight to /app, which immediately resolves a
|
|
207
|
+
// workspace scope. Creating it here (not lazily on that first request) means a sign-in that
|
|
208
|
+
// cannot produce a usable tenant fails at sign-in, where the error is attributable.
|
|
209
|
+
await ensureAccountWorkspace(account.id);
|
|
210
|
+
return account;
|
|
211
|
+
}
|
|
212
|
+
// In-memory throttle for the activity stamp below: accountId → last-stamp epoch ms.
|
|
213
|
+
// A loaded SPA is chatty (timeline, polls, heartbeat), so we only touch the DB at
|
|
214
|
+
// most once per window per account.
|
|
215
|
+
const lastActiveStampMs = new Map();
|
|
216
|
+
const ACTIVE_STAMP_THROTTLE_MS = 60_000;
|
|
217
|
+
/**
|
|
218
|
+
* Record that a loaded frontend for this account just talked to the backend
|
|
219
|
+
* (drives the scheduler's "only sync accounts with an open tab" gate). Throttled
|
|
220
|
+
* in-memory and fire-and-forget — a dropped stamp only means a slightly staler
|
|
221
|
+
* signal, and the next request re-stamps. Cloud-only in practice (the caller gates
|
|
222
|
+
* on isCloud; local has a single always-synced account).
|
|
223
|
+
*/
|
|
224
|
+
export function stampAccountActive(accountId) {
|
|
225
|
+
const now = Date.now();
|
|
226
|
+
const last = lastActiveStampMs.get(accountId) ?? 0;
|
|
227
|
+
if (now - last < ACTIVE_STAMP_THROTTLE_MS)
|
|
228
|
+
return;
|
|
229
|
+
lastActiveStampMs.set(accountId, now);
|
|
230
|
+
const { accounts } = schema;
|
|
231
|
+
void db
|
|
232
|
+
.update(accounts)
|
|
233
|
+
.set({ lastActiveAt: new Date() })
|
|
234
|
+
.where(eq(accounts.id, accountId))
|
|
235
|
+
.execute()
|
|
236
|
+
.catch(() => {
|
|
237
|
+
// Best-effort: roll back the throttle so the next request retries the stamp.
|
|
238
|
+
lastActiveStampMs.delete(accountId);
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
/** Load an account by id. */
|
|
242
|
+
export async function getAccountById(id) {
|
|
243
|
+
const { accounts } = schema;
|
|
244
|
+
const rows = await db
|
|
245
|
+
.select()
|
|
246
|
+
.from(accounts)
|
|
247
|
+
.where(eq(accounts.id, id))
|
|
248
|
+
.limit(1)
|
|
249
|
+
.execute();
|
|
250
|
+
return rows[0] ? rowToAccount(rows[0]) : null;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Set an account's billing plan (and, when known, its Stripe customer id).
|
|
254
|
+
* Called only by the Stripe webhook handler (api/routes/billing.ts).
|
|
255
|
+
*/
|
|
256
|
+
export async function setAccountPlan(accountId, plan, stripeCustomerId) {
|
|
257
|
+
const { accounts } = schema;
|
|
258
|
+
const set = { plan };
|
|
259
|
+
if (stripeCustomerId != null)
|
|
260
|
+
set.stripeCustomerId = stripeCustomerId;
|
|
261
|
+
await db.update(accounts).set(set).where(eq(accounts.id, accountId)).execute();
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Set an account's cross-org benchmark consent (cloud-only feature). Withdrawing (optIn=false)
|
|
265
|
+
* DELETES the account's contributions — one-click, complete removal, honouring the consent
|
|
266
|
+
* promise. The caller (the /api/me/benchmark-consent route) seeds contributions on opt-in.
|
|
267
|
+
*/
|
|
268
|
+
export async function setBenchmarkConsent(accountId, optIn) {
|
|
269
|
+
const { accounts } = schema;
|
|
270
|
+
await db
|
|
271
|
+
.update(accounts)
|
|
272
|
+
.set({ benchmarkOptIn: optIn })
|
|
273
|
+
.where(eq(accounts.id, accountId))
|
|
274
|
+
.execute();
|
|
275
|
+
if (!optIn) {
|
|
276
|
+
const { deleteBenchmarkContributions } = await import('../db/queries.js');
|
|
277
|
+
await deleteBenchmarkContributions(accountId);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Set (or clear) an account's LARGE-PR FLAG threshold, in lines of CODE churn.
|
|
282
|
+
*
|
|
283
|
+
* `null` CLEARS it — back to the two-state "no opinion → product default", which is what makes a
|
|
284
|
+
* later change to that default reach every account that never overrode it. It is a column write,
|
|
285
|
+
* never a delete of anything, and it is deliberately validated HERE as well as by the route's
|
|
286
|
+
* schema: a threshold of 0 or a fraction would flag every PR or none, silently.
|
|
287
|
+
*/
|
|
288
|
+
export async function setLargePrCodeLocThreshold(accountId, threshold) {
|
|
289
|
+
const { accounts } = schema;
|
|
290
|
+
const value = threshold != null && Number.isInteger(threshold) && threshold > 0 ? threshold : null;
|
|
291
|
+
await db
|
|
292
|
+
.update(accounts)
|
|
293
|
+
.set({ largePrCodeLocThreshold: value })
|
|
294
|
+
.where(eq(accounts.id, accountId))
|
|
295
|
+
.execute();
|
|
296
|
+
// ⚠ REQUIRED, not tidiness — see refreshLocalAccountCache. Without it this setting appears not
|
|
297
|
+
// to save in local mode: the route 200s with the new number and the SPA's own `['me']` refetch
|
|
298
|
+
// immediately reports the old one.
|
|
299
|
+
await refreshLocalAccountCache(accountId);
|
|
300
|
+
}
|
|
301
|
+
// The runtime spellings of two `packages/shared` unions. ⚠ MIRRORED ON PURPOSE, and this is the
|
|
302
|
+
// small mirror the design chose over the big one: `shared` is types-only on this side
|
|
303
|
+
// (PACKAGING), so a route that validates a stored enum needs the members as VALUES. Ten strings
|
|
304
|
+
// and three, versus the 18-number BLAST_THRESHOLDS table the SPA resolves instead. If a member is
|
|
305
|
+
// added to either union in shared, add it here — `blast-radius-config.test.ts` pins the count so
|
|
306
|
+
// the omission fails rather than silently rejecting a valid write.
|
|
307
|
+
const BLAST_SURFACE_VALUES = [
|
|
308
|
+
'db_migration',
|
|
309
|
+
'db_schema',
|
|
310
|
+
'sql',
|
|
311
|
+
'public_types',
|
|
312
|
+
'idl',
|
|
313
|
+
'openapi',
|
|
314
|
+
'infra',
|
|
315
|
+
'auth',
|
|
316
|
+
'ci',
|
|
317
|
+
'deps',
|
|
318
|
+
];
|
|
319
|
+
const BLAST_SENSITIVITY_VALUES = ['cautious', 'balanced', 'relaxed'];
|
|
320
|
+
const BLAST_THRESHOLD_KEYS = [
|
|
321
|
+
'highCodeLoc',
|
|
322
|
+
'highCodeFiles',
|
|
323
|
+
'highDirs',
|
|
324
|
+
'highSubsystems',
|
|
325
|
+
'lowCodeLoc',
|
|
326
|
+
'lowCodeFiles',
|
|
327
|
+
];
|
|
328
|
+
/**
|
|
329
|
+
* Validate an untrusted blast-radius config into the shape the column may hold, or `null`.
|
|
330
|
+
*
|
|
331
|
+
* ⚠ RETURNS null FOR ANYTHING IT CANNOT FULLY VALIDATE, which is the same "no opinion" state a
|
|
332
|
+
* cleared setting has — never a partially-applied blob. An unknown surface string is DROPPED
|
|
333
|
+
* rather than failing the whole write, so an older backend reading a newer client's payload
|
|
334
|
+
* degrades to ignoring one opt-out instead of discarding the user's dial.
|
|
335
|
+
*/
|
|
336
|
+
export function sanitizeBlastRadiusConfig(input) {
|
|
337
|
+
if (input == null || typeof input !== 'object' || Array.isArray(input))
|
|
338
|
+
return null;
|
|
339
|
+
const raw = input;
|
|
340
|
+
const sensitivity = BLAST_SENSITIVITY_VALUES.find((v) => v === raw.sensitivity);
|
|
341
|
+
if (sensitivity == null)
|
|
342
|
+
return null;
|
|
343
|
+
const surfacesOff = Array.isArray(raw.surfacesOff)
|
|
344
|
+
? BLAST_SURFACE_VALUES.filter((v) => raw.surfacesOff.includes(v))
|
|
345
|
+
: [];
|
|
346
|
+
let overrides;
|
|
347
|
+
if (raw.overrides != null && typeof raw.overrides === 'object' && !Array.isArray(raw.overrides)) {
|
|
348
|
+
const src = raw.overrides;
|
|
349
|
+
const out = {};
|
|
350
|
+
for (const k of BLAST_THRESHOLD_KEYS) {
|
|
351
|
+
const v = src[k];
|
|
352
|
+
// Same rule as the large-PR threshold: a 0 or a fraction would bucket everything or
|
|
353
|
+
// nothing, silently. Only a positive integer is an opinion.
|
|
354
|
+
if (typeof v === 'number' && Number.isInteger(v) && v > 0)
|
|
355
|
+
out[k] = v;
|
|
356
|
+
}
|
|
357
|
+
if (Object.keys(out).length > 0)
|
|
358
|
+
overrides = out;
|
|
359
|
+
}
|
|
360
|
+
// ⚠ ONLY STORED WHEN IT IS `false`. `showImpactNote` defaults to SHOWN, so writing `true`
|
|
361
|
+
// would persist the product default as a choice — the same two-state rule the whole config
|
|
362
|
+
// follows, one field down. A blob whose only content was `{showImpactNote: true}` would also
|
|
363
|
+
// stop `isDefault` reading true in Settings for a user who changed nothing that matters.
|
|
364
|
+
const hide = raw.showImpactNote === false;
|
|
365
|
+
const base = { sensitivity, surfacesOff };
|
|
366
|
+
if (overrides)
|
|
367
|
+
base.overrides = overrides;
|
|
368
|
+
if (hide)
|
|
369
|
+
base.showImpactNote = false;
|
|
370
|
+
return base;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Set (or clear) an account's BLAST-RADIUS reading settings.
|
|
374
|
+
*
|
|
375
|
+
* `null` — and any payload that fails validation — CLEARS the column, back to the two-state
|
|
376
|
+
* "no opinion → product defaults". Exactly the rule `setLargePrCodeLocThreshold` follows, and for
|
|
377
|
+
* the same reason: a stored blob would freeze this account against a later change to those
|
|
378
|
+
* defaults.
|
|
379
|
+
*/
|
|
380
|
+
export async function setBlastRadiusConfig(accountId, config) {
|
|
381
|
+
const { accounts } = schema;
|
|
382
|
+
const value = sanitizeBlastRadiusConfig(config);
|
|
383
|
+
await db
|
|
384
|
+
.update(accounts)
|
|
385
|
+
.set({ blastRadiusConfig: value })
|
|
386
|
+
.where(eq(accounts.id, accountId))
|
|
387
|
+
.execute();
|
|
388
|
+
// ⚠ Same rule as the threshold above, and the same failure without it.
|
|
389
|
+
await refreshLocalAccountCache(accountId);
|
|
390
|
+
return value;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Set (or clear) an account's MY TURN settings, and return what was STORED.
|
|
394
|
+
*
|
|
395
|
+
* The value is COMPACTED first (`compactMyTurnSettings`, the ONE definition of "an override" the
|
|
396
|
+
* Settings form builds its body with too), so a default is never frozen into the row — `null`, or
|
|
397
|
+
* anything that compacts to nothing, stores NULL and the account follows the product defaults
|
|
398
|
+
* again. Validate BEFORE calling: compaction drops a malformed part silently, which is right for a
|
|
399
|
+
* stored value and wrong for a request.
|
|
400
|
+
*/
|
|
401
|
+
export async function setMyTurnSettings(accountId, settings) {
|
|
402
|
+
const { accounts } = schema;
|
|
403
|
+
const value = compactMyTurnSettings(settings);
|
|
404
|
+
await db
|
|
405
|
+
.update(accounts)
|
|
406
|
+
.set({ myTurnSettings: value })
|
|
407
|
+
.where(eq(accounts.id, accountId))
|
|
408
|
+
.execute();
|
|
409
|
+
// ⚠ Same rule as the two settings above, and the same failure without it: Settings would save,
|
|
410
|
+
// then `/api/me` would hand the old value back and revert the form.
|
|
411
|
+
await refreshLocalAccountCache(accountId);
|
|
412
|
+
return value;
|
|
413
|
+
}
|
|
414
|
+
/** Resolve an account by its Stripe customer id (subscription webhooks). */
|
|
415
|
+
export async function getAccountByStripeCustomerId(customerId) {
|
|
416
|
+
const { accounts } = schema;
|
|
417
|
+
const rows = await db
|
|
418
|
+
.select()
|
|
419
|
+
.from(accounts)
|
|
420
|
+
.where(eq(accounts.stripeCustomerId, customerId))
|
|
421
|
+
.limit(1)
|
|
422
|
+
.execute();
|
|
423
|
+
return rows[0] ? rowToAccount(rows[0]) : null;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Resolve an account's owner to a row in `users` (by login) if they've appeared
|
|
427
|
+
* in any synced repo. Returns null when "you" haven't authored/acted anywhere
|
|
428
|
+
* yet. This is the "who am I" used by all triage ("my turn").
|
|
429
|
+
*/
|
|
430
|
+
export async function getAccountUserId(accountId) {
|
|
431
|
+
const account = await getAccountById(accountId);
|
|
432
|
+
if (!account || !account.githubLogin)
|
|
433
|
+
return null;
|
|
434
|
+
const { users } = schema;
|
|
435
|
+
const rows = await db
|
|
436
|
+
.select({ id: users.id })
|
|
437
|
+
.from(users)
|
|
438
|
+
.where(eq(users.githubLogin, account.githubLogin))
|
|
439
|
+
.limit(1)
|
|
440
|
+
.execute();
|
|
441
|
+
return rows[0]?.id ?? null;
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Resolve the GitHub access token to use for an account's API calls.
|
|
445
|
+
* - Local account (isLocal): live `gh auth token` (no token stored).
|
|
446
|
+
* - Cloud account: decrypt the stored AES-256-GCM sealed token.
|
|
447
|
+
* Throws if the account is missing or a cloud account has no stored token.
|
|
448
|
+
*/
|
|
449
|
+
export async function getAccessToken(accountId) {
|
|
450
|
+
const { accounts } = schema;
|
|
451
|
+
const rows = await db
|
|
452
|
+
.select()
|
|
453
|
+
.from(accounts)
|
|
454
|
+
.where(eq(accounts.id, accountId))
|
|
455
|
+
.limit(1)
|
|
456
|
+
.execute();
|
|
457
|
+
const row = rows[0];
|
|
458
|
+
if (!row)
|
|
459
|
+
throw new Error(`account ${accountId} not found`);
|
|
460
|
+
// The ASYNC form: this runs on request paths (PR-detail hydration, repo search, every write
|
|
461
|
+
// action), and the synchronous `gh auth token` it used to call blocked the event loop for
|
|
462
|
+
// 50–300ms per call while forking a child process. Both are now cached with a short TTL and
|
|
463
|
+
// share one in-flight invocation — see github/auth.ts.
|
|
464
|
+
if (row.isLocal)
|
|
465
|
+
return getGithubTokenAsync();
|
|
466
|
+
if (!row.accessTokenEnc) {
|
|
467
|
+
throw new Error(`account ${accountId} has no stored access token (re-auth needed)`);
|
|
468
|
+
}
|
|
469
|
+
return decryptToken(row.accessTokenEnc);
|
|
470
|
+
}
|
|
471
|
+
/** Shape an Account as the legacy LocalUser wire type for /api/me. */
|
|
472
|
+
export function accountToLocalUser(account) {
|
|
473
|
+
if (!account || !account.githubLogin)
|
|
474
|
+
return null;
|
|
475
|
+
return {
|
|
476
|
+
login: account.githubLogin,
|
|
477
|
+
githubId: account.githubUserId,
|
|
478
|
+
avatarUrl: account.avatarUrl,
|
|
479
|
+
displayName: account.displayName,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
//# sourceMappingURL=account.js.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
|
|
2
|
+
import { config } from '../config.js';
|
|
3
|
+
// AES-256-GCM sealing of per-account GitHub access tokens at rest. The key is
|
|
4
|
+
// ENCRYPTION_KEY (32 bytes as 64 hex chars; generate with `openssl rand -hex 32`).
|
|
5
|
+
// Stored form is `iv:tag:ciphertext`, each base64. Tokens are decrypted in-memory
|
|
6
|
+
// per call and NEVER logged.
|
|
7
|
+
const ALGO = 'aes-256-gcm';
|
|
8
|
+
function key() {
|
|
9
|
+
const k = Buffer.from(config.encryptionKey, 'hex');
|
|
10
|
+
if (k.length !== 32) {
|
|
11
|
+
throw new Error('ENCRYPTION_KEY must be 32 bytes as 64 hex chars (openssl rand -hex 32).');
|
|
12
|
+
}
|
|
13
|
+
return k;
|
|
14
|
+
}
|
|
15
|
+
export function encryptToken(plain) {
|
|
16
|
+
const iv = randomBytes(12);
|
|
17
|
+
const cipher = createCipheriv(ALGO, key(), iv);
|
|
18
|
+
const ciphertext = Buffer.concat([cipher.update(plain, 'utf8'), cipher.final()]);
|
|
19
|
+
const tag = cipher.getAuthTag();
|
|
20
|
+
return [
|
|
21
|
+
iv.toString('base64'),
|
|
22
|
+
tag.toString('base64'),
|
|
23
|
+
ciphertext.toString('base64'),
|
|
24
|
+
].join(':');
|
|
25
|
+
}
|
|
26
|
+
export function decryptToken(enc) {
|
|
27
|
+
const parts = enc.split(':');
|
|
28
|
+
if (parts.length !== 3)
|
|
29
|
+
throw new Error('malformed encrypted token');
|
|
30
|
+
const [ivB64, tagB64, ctB64] = parts;
|
|
31
|
+
const decipher = createDecipheriv(ALGO, key(), Buffer.from(ivB64, 'base64'));
|
|
32
|
+
decipher.setAuthTag(Buffer.from(tagB64, 'base64'));
|
|
33
|
+
return Buffer.concat([
|
|
34
|
+
decipher.update(Buffer.from(ctB64, 'base64')),
|
|
35
|
+
decipher.final(),
|
|
36
|
+
]).toString('utf8');
|
|
37
|
+
}
|
|
38
|
+
// Whether sealing is available on this process: ENCRYPTION_KEY is set and is 32 bytes. Always true
|
|
39
|
+
// in cloud (`assertCloudConfig` refuses to boot otherwise); true locally only when the operator set
|
|
40
|
+
// a key. The Pro plugin's optional `host.sealSecret` / `host.openSecret` seam is wired from this, so
|
|
41
|
+
// a plugin-stored secret (the per-workspace Jira token) is sealed wherever a key exists and stored
|
|
42
|
+
// plain only on a local install without one — the same trust as that machine's `gh` token.
|
|
43
|
+
export function sealingAvailable() {
|
|
44
|
+
return Buffer.from(config.encryptionKey, 'hex').length === 32;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=crypto.js.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { getAccessToken } from '../auth/account.js';
|
|
2
|
+
import { setReviewThreadResolved } from '../github/mutations.js';
|
|
3
|
+
import { stampThreadResolved } from '../db/queries.js';
|
|
4
|
+
import { notePrChangedForPrs } from '../sync/pr-settle.js';
|
|
5
|
+
// Resolve a batch of already-eligible review threads on GitHub AND stamp them resolved
|
|
6
|
+
// locally. This is the ONE place the GitHub `resolveReviewThread` mutation is driven for
|
|
7
|
+
// bot-thread triage — shared verbatim by the manual per-PR `POST /api/prs/:id/resolve-bot-
|
|
8
|
+
// threads` route and the workspace-wide `POST /api/bot-threads/resolve`, so their behaviour
|
|
9
|
+
// can't drift. Both are strictly user-initiated + confirm-gated — there is no automatic/cron
|
|
10
|
+
// resolve.
|
|
11
|
+
//
|
|
12
|
+
// Callers own eligibility: `getResolvableBotThreads` (per-PR — the workspace comes from the PR's
|
|
13
|
+
// own repo, via `botScopeForPr`) / `getResolvableBotThreadsForScope` (workspace-wide — takes the
|
|
14
|
+
// `BotScope` the route built with `resolveWorkspaceScope`) return ONLY owned + automated-reviewer-
|
|
15
|
+
// originated + `likely_addressed` + unresolved threads (with a GitHub node id), so this helper
|
|
16
|
+
// never has to re-derive it.
|
|
17
|
+
//
|
|
18
|
+
// ⚠ THE CALLER'S RE-DERIVATION MUST RUN AT THE SAME WORKSPACE THE LISTING OFFERED FROM. "Is this
|
|
19
|
+
// login a bot" is answered once per workspace, so a listing resolved at workspace A and a resolve
|
|
20
|
+
// re-derived at workspace B disagree about which threads are bot threads — the listing offers a
|
|
21
|
+
// backlog and the resolve then finds nothing eligible, returning 0 with no error anywhere. That
|
|
22
|
+
// was a real, already-closed gap and it must not reopen: `ScopeResolveBotThreadsBody.workspaceId`
|
|
23
|
+
// is what makes the two sides structurally the same scope, and this helper cannot check it — by
|
|
24
|
+
// the time threads reach here the judgement has already been made.
|
|
25
|
+
//
|
|
26
|
+
// It NEVER merges — it only resolves. Sequential (not Promise.all) to stay gentle on the GraphQL
|
|
27
|
+
// rate limit (a bot backlog is a handful of threads); per-thread try/catch so one bad thread or a
|
|
28
|
+
// moved head never aborts the batch. The token is fetched once per batch from the OWNING account,
|
|
29
|
+
// so a bad/absent token throws BEFORE the loop — the caller (route or job) decides how to surface it.
|
|
30
|
+
//
|
|
31
|
+
// Each thread names its PR (`prId`, from the caller's own eligibility read) so the helper can raise
|
|
32
|
+
// the SPA change signal for every repo it stamped, ONCE, before the route replies — the stamps move
|
|
33
|
+
// the Pending thread cards, and no walk will report them (sync/pr-settle.ts `notePrChangedForPrs`).
|
|
34
|
+
export async function resolveThreadsOnGitHub(accountId, threads) {
|
|
35
|
+
const out = { resolved: 0, failed: 0, results: [] };
|
|
36
|
+
// Empty selection → a no-op (never even fetch a token) — matches the manual route's
|
|
37
|
+
// "stale client list / no eligible threads is a no-op, not an error" contract.
|
|
38
|
+
if (threads.length === 0)
|
|
39
|
+
return out;
|
|
40
|
+
const token = await getAccessToken(accountId);
|
|
41
|
+
const stampedPrIds = new Set();
|
|
42
|
+
for (const t of threads) {
|
|
43
|
+
try {
|
|
44
|
+
await setReviewThreadResolved(token, t.threadNodeId, true);
|
|
45
|
+
const derivedState = await stampThreadResolved(t.id, true, accountId);
|
|
46
|
+
out.results.push({ threadId: t.id, ok: true, derivedState: derivedState ?? 'resolved' });
|
|
47
|
+
out.resolved += 1;
|
|
48
|
+
stampedPrIds.add(t.prId);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
out.results.push({ threadId: t.id, ok: false, derivedState: null });
|
|
52
|
+
out.failed += 1;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Never throws, and a no-op when nothing was stamped.
|
|
56
|
+
await notePrChangedForPrs(accountId, [...stampedPrIds]);
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=resolve.js.map
|