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,996 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { and, asc, desc, eq, gte, inArray, isNull, lt, ne, or, sql } from 'drizzle-orm';
|
|
3
|
+
import { CLAUDE_FINDING_LENSES, threadAssessmentCounts } from '../../shared/index.js';
|
|
4
|
+
import { storedList, stripStoredStoryLead } from '../../shared/index.js';
|
|
5
|
+
import { ticketEntriesOf } from './ticket.js';
|
|
6
|
+
import { isFollowUpEligible, resolveFindingBody, } from './follow-up.js';
|
|
7
|
+
// The Claude Review persistence layer. It reads + writes the claudeReviews /
|
|
8
|
+
// claudeReviewFindings tables via ctx.db + ctx.schema (review/agent-context.ts). Only the
|
|
9
|
+
// FEATURE-only reads live here; the cross-surface reads (getClaudeReviewFeedItems /
|
|
10
|
+
// getUnactionedClaudeReviews / listClaudeReviewsByRepo) live in db/queries.ts.
|
|
11
|
+
// ctx.schema is typed loosely (Record<string, any>) — this module addresses tables by name and
|
|
12
|
+
// casts result rows, so it compiles identically against either dialect's schema.
|
|
13
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
14
|
+
function tables(ctx) {
|
|
15
|
+
const s = ctx.schema;
|
|
16
|
+
return { cr: s.claudeReviews, crf: s.claudeReviewFindings, prs: s.pullRequests, repos: s.repos };
|
|
17
|
+
}
|
|
18
|
+
const iso = (d) => d instanceof Date ? d.toISOString() : d == null ? null : new Date(d).toISOString();
|
|
19
|
+
const isoReq = (d) => d instanceof Date ? d.toISOString() : new Date(d).toISOString();
|
|
20
|
+
// GitHub anchors a file in the PR "Files changed" diff by the SHA-256 of its path.
|
|
21
|
+
function diffAnchorId(path) {
|
|
22
|
+
return createHash('sha256').update(path, 'utf8').digest('hex');
|
|
23
|
+
}
|
|
24
|
+
// A story finding's origin off its row; null unless BOTH columns hold a value.
|
|
25
|
+
export function storyOf(r) {
|
|
26
|
+
return r.storyIndex != null && Number.isInteger(r.storyIndex) && r.storyIndex >= 0 && r.storyRef
|
|
27
|
+
? { index: r.storyIndex, ref: r.storyRef }
|
|
28
|
+
: null;
|
|
29
|
+
}
|
|
30
|
+
// A stored CI record, or null for an older row / anything malformed (never a guessed shape).
|
|
31
|
+
export function ciRecordOf(v) {
|
|
32
|
+
if (v == null || typeof v !== 'object')
|
|
33
|
+
return null;
|
|
34
|
+
const r = v;
|
|
35
|
+
if (!Array.isArray(r.failures) || typeof r.state !== 'string')
|
|
36
|
+
return null;
|
|
37
|
+
return {
|
|
38
|
+
state: r.state,
|
|
39
|
+
checkCount: typeof r.checkCount === 'number' ? r.checkCount : 0,
|
|
40
|
+
failures: r.failures,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function mapFinding(r) {
|
|
44
|
+
return {
|
|
45
|
+
id: r.id,
|
|
46
|
+
reviewId: r.reviewId,
|
|
47
|
+
path: r.path,
|
|
48
|
+
line: r.line,
|
|
49
|
+
side: r.side,
|
|
50
|
+
diffAnchorId: diffAnchorId(r.path),
|
|
51
|
+
severity: r.severity,
|
|
52
|
+
title: r.title,
|
|
53
|
+
// A story finding stored before its lead moved to post time began with that lead, which only
|
|
54
|
+
// repeated the title on screen (shared `stripStoredStoryLead`).
|
|
55
|
+
body: stripStoredStoryLead(r.body, storyOf(r)),
|
|
56
|
+
editedBody: r.editedBody,
|
|
57
|
+
suggestion: r.suggestion,
|
|
58
|
+
diffHunk: r.diffHunk,
|
|
59
|
+
anchored: r.anchored,
|
|
60
|
+
fileInDiff: r.fileInDiff,
|
|
61
|
+
included: r.included,
|
|
62
|
+
postedAt: iso(r.postedAt),
|
|
63
|
+
githubCommentId: r.githubCommentId,
|
|
64
|
+
postedCommentKind: r.postedCommentKind,
|
|
65
|
+
createdAt: isoReq(r.createdAt),
|
|
66
|
+
priorFindingId: r.priorFindingId ?? null,
|
|
67
|
+
lens: asLens(r.lens),
|
|
68
|
+
story: storyOf(r),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
// A stored lens outside today's list reads as a general finding, never a made-up label.
|
|
72
|
+
function asLens(v) {
|
|
73
|
+
return v != null && CLAUDE_FINDING_LENSES.includes(v)
|
|
74
|
+
? v
|
|
75
|
+
: null;
|
|
76
|
+
}
|
|
77
|
+
// The stored follow-up + each item's DERIVED `reraisedFindingId` (the first of this run's
|
|
78
|
+
// findings whose priorFindingId points at the item's earlier finding).
|
|
79
|
+
function mapFollowUp(fu, findings) {
|
|
80
|
+
if (!fu)
|
|
81
|
+
return null;
|
|
82
|
+
const firstByPrior = new Map();
|
|
83
|
+
for (const f of findings) {
|
|
84
|
+
if (f.priorFindingId != null && !firstByPrior.has(f.priorFindingId)) {
|
|
85
|
+
firstByPrior.set(f.priorFindingId, f.id);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
...fu,
|
|
90
|
+
items: (fu.items ?? []).map((it) => ({
|
|
91
|
+
...it,
|
|
92
|
+
reraisedFindingId: firstByPrior.get(it.priorFindingId) ?? null,
|
|
93
|
+
})),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function mapReview(r, findings, head = null) {
|
|
97
|
+
const tickets = ticketEntriesOf(r.ticket, r.ticketAssessment);
|
|
98
|
+
return {
|
|
99
|
+
id: r.id,
|
|
100
|
+
prId: r.prId,
|
|
101
|
+
headSha: r.headSha,
|
|
102
|
+
status: r.status,
|
|
103
|
+
model: r.model,
|
|
104
|
+
scope: r.scope,
|
|
105
|
+
reviewMode: r.reviewMode,
|
|
106
|
+
routeReason: r.routeReason,
|
|
107
|
+
summary: r.summary,
|
|
108
|
+
verdict: r.verdict,
|
|
109
|
+
userBody: r.userBody,
|
|
110
|
+
userVerdict: r.userVerdict,
|
|
111
|
+
costUsd: r.costUsd,
|
|
112
|
+
inputTokens: r.inputTokens,
|
|
113
|
+
outputTokens: r.outputTokens,
|
|
114
|
+
cacheReadTokens: r.cacheReadTokens,
|
|
115
|
+
cacheCreationTokens: r.cacheCreationTokens,
|
|
116
|
+
numTurns: r.numTurns,
|
|
117
|
+
diffBytes: r.diffBytes,
|
|
118
|
+
diffCapped: r.diffCapped,
|
|
119
|
+
error: r.error,
|
|
120
|
+
excludedFiles: r.excludedFiles ?? [],
|
|
121
|
+
postedReviewId: r.postedReviewId,
|
|
122
|
+
postedAt: iso(r.postedAt),
|
|
123
|
+
createdAt: isoReq(r.createdAt),
|
|
124
|
+
finishedAt: iso(r.finishedAt),
|
|
125
|
+
findings: findings.map(mapFinding),
|
|
126
|
+
tickets,
|
|
127
|
+
ticket: tickets[0]?.ticket ?? null,
|
|
128
|
+
ticketAssessment: tickets[0]?.assessment ?? null,
|
|
129
|
+
head,
|
|
130
|
+
followUp: mapFollowUp(r.followUp, findings),
|
|
131
|
+
trigger: r.trigger === 'auto' ? 'auto' : 'manual',
|
|
132
|
+
// null = this run did not assess threads (older row, skip, not succeeded) — never [].
|
|
133
|
+
threadAssessments: Array.isArray(r.threadAssessments) ? r.threadAssessments : null,
|
|
134
|
+
threadAssessmentCounts: Array.isArray(r.threadAssessments)
|
|
135
|
+
? threadAssessmentCounts(r.threadAssessments)
|
|
136
|
+
: null,
|
|
137
|
+
// null = this run did not look at CI (older row, skip, not succeeded, checks unreadable).
|
|
138
|
+
...ciWireOf(r.ciFailures),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function ciWireOf(v) {
|
|
142
|
+
const rec = ciRecordOf(v);
|
|
143
|
+
return rec
|
|
144
|
+
? { ciFailures: rec.failures, ciState: { state: rec.state, checkCount: rec.checkCount } }
|
|
145
|
+
: { ciFailures: null, ciState: null };
|
|
146
|
+
}
|
|
147
|
+
const summaryOf = (r) => ({
|
|
148
|
+
id: r.id,
|
|
149
|
+
headSha: r.headSha,
|
|
150
|
+
status: r.status,
|
|
151
|
+
model: r.model,
|
|
152
|
+
scope: r.scope,
|
|
153
|
+
reviewMode: r.reviewMode,
|
|
154
|
+
verdict: r.verdict,
|
|
155
|
+
userVerdict: r.userVerdict,
|
|
156
|
+
costUsd: r.costUsd,
|
|
157
|
+
postedAt: iso(r.postedAt),
|
|
158
|
+
createdAt: isoReq(r.createdAt),
|
|
159
|
+
finishedAt: iso(r.finishedAt),
|
|
160
|
+
trigger: r.trigger === 'auto' ? 'auto' : 'manual',
|
|
161
|
+
});
|
|
162
|
+
// ---- Reads (account-scoped via the join up to repos.accountId — IDOR guard) ----
|
|
163
|
+
export async function getClaudeReviewById(ctx, reviewId, accountId) {
|
|
164
|
+
const { cr, crf, prs, repos } = tables(ctx);
|
|
165
|
+
const rows = (await ctx.db
|
|
166
|
+
.select({ review: cr, prHeadSha: prs.headSha })
|
|
167
|
+
.from(cr)
|
|
168
|
+
.innerJoin(prs, eq(prs.id, cr.prId))
|
|
169
|
+
.innerJoin(repos, eq(repos.id, prs.repoId))
|
|
170
|
+
.where(and(eq(cr.id, reviewId), eq(repos.accountId, accountId)))
|
|
171
|
+
.limit(1)
|
|
172
|
+
.execute());
|
|
173
|
+
const row = rows[0]?.review ?? null;
|
|
174
|
+
if (!row)
|
|
175
|
+
return null;
|
|
176
|
+
const findings = (await ctx.db
|
|
177
|
+
.select()
|
|
178
|
+
.from(crf)
|
|
179
|
+
.where(eq(crf.reviewId, reviewId))
|
|
180
|
+
.orderBy(asc(crf.id))
|
|
181
|
+
.execute());
|
|
182
|
+
const head = await reviewHeadState(ctx, row.prId, row.headSha, rows[0].prHeadSha);
|
|
183
|
+
return mapReview(row, findings, head);
|
|
184
|
+
}
|
|
185
|
+
export async function getLatestClaudeReview(ctx, prId, accountId) {
|
|
186
|
+
const { cr, prs, repos } = tables(ctx);
|
|
187
|
+
const rows = (await ctx.db
|
|
188
|
+
.select({ id: cr.id })
|
|
189
|
+
.from(cr)
|
|
190
|
+
.innerJoin(prs, eq(prs.id, cr.prId))
|
|
191
|
+
.innerJoin(repos, eq(repos.id, prs.repoId))
|
|
192
|
+
.where(and(eq(cr.prId, prId), eq(repos.accountId, accountId)))
|
|
193
|
+
.orderBy(desc(cr.id))
|
|
194
|
+
.limit(1)
|
|
195
|
+
.execute());
|
|
196
|
+
const row = rows[0] ?? null;
|
|
197
|
+
if (!row)
|
|
198
|
+
return null;
|
|
199
|
+
return getClaudeReviewById(ctx, row.id, accountId);
|
|
200
|
+
}
|
|
201
|
+
export async function listClaudeReviewHistory(ctx, prId, accountId) {
|
|
202
|
+
const { cr, prs, repos } = tables(ctx);
|
|
203
|
+
const rows = (await ctx.db
|
|
204
|
+
.select({ review: cr })
|
|
205
|
+
.from(cr)
|
|
206
|
+
.innerJoin(prs, eq(prs.id, cr.prId))
|
|
207
|
+
.innerJoin(repos, eq(repos.id, prs.repoId))
|
|
208
|
+
.where(and(eq(cr.prId, prId), eq(repos.accountId, accountId)))
|
|
209
|
+
.orderBy(desc(cr.id))
|
|
210
|
+
.execute());
|
|
211
|
+
return rows.map(({ review }) => summaryOf(review));
|
|
212
|
+
}
|
|
213
|
+
// The window that scopes the cross-PR list (mirrors the core backfill default of 90d).
|
|
214
|
+
const LIST_WINDOW_DAYS = Number.parseInt(process.env.PRO_REVIEW_LIST_WINDOW_DAYS ?? '', 10) || 90;
|
|
215
|
+
export async function listAllClaudeReviews(ctx, accountId) {
|
|
216
|
+
const { cr, prs, repos } = tables(ctx);
|
|
217
|
+
const cutoff = new Date(Date.now() - LIST_WINDOW_DAYS * 24 * 60 * 60 * 1000);
|
|
218
|
+
// sqlite timestamps are unix SECONDS; pg is a Date. Bind the dialect-appropriate value.
|
|
219
|
+
const tsBound = ctx.isPg ? cutoff : Math.floor(cutoff.getTime() / 1000);
|
|
220
|
+
const rows = (await ctx.db
|
|
221
|
+
.select({
|
|
222
|
+
reviewId: cr.id,
|
|
223
|
+
prId: cr.prId,
|
|
224
|
+
owner: repos.owner,
|
|
225
|
+
name: repos.name,
|
|
226
|
+
prNumber: prs.number,
|
|
227
|
+
prTitle: prs.title,
|
|
228
|
+
prState: prs.state,
|
|
229
|
+
summary: cr.summary,
|
|
230
|
+
verdict: cr.verdict,
|
|
231
|
+
headSha: cr.headSha,
|
|
232
|
+
status: cr.status,
|
|
233
|
+
createdAt: cr.createdAt,
|
|
234
|
+
finishedAt: cr.finishedAt,
|
|
235
|
+
})
|
|
236
|
+
.from(cr)
|
|
237
|
+
.innerJoin(prs, eq(prs.id, cr.prId))
|
|
238
|
+
.innerJoin(repos, eq(repos.id, prs.repoId))
|
|
239
|
+
.where(and(eq(repos.accountId, accountId), eq(cr.status, 'succeeded'), or(eq(prs.state, 'open'), gte(sql `coalesce(${prs.mergedAt}, ${prs.closedAt}, ${prs.openedAt})`, tsBound))))
|
|
240
|
+
.orderBy(desc(cr.finishedAt), desc(cr.createdAt))
|
|
241
|
+
.execute());
|
|
242
|
+
const seen = new Set();
|
|
243
|
+
const items = [];
|
|
244
|
+
for (const r of rows) {
|
|
245
|
+
if (seen.has(r.prId))
|
|
246
|
+
continue;
|
|
247
|
+
seen.add(r.prId);
|
|
248
|
+
items.push({
|
|
249
|
+
reviewId: r.reviewId,
|
|
250
|
+
prId: r.prId,
|
|
251
|
+
repoFullName: `${r.owner}/${r.name}`,
|
|
252
|
+
prNumber: r.prNumber,
|
|
253
|
+
prTitle: r.prTitle,
|
|
254
|
+
prState: r.prState,
|
|
255
|
+
summary: r.summary,
|
|
256
|
+
verdict: r.verdict,
|
|
257
|
+
headSha: r.headSha,
|
|
258
|
+
status: r.status,
|
|
259
|
+
createdAt: isoReq(r.createdAt),
|
|
260
|
+
finishedAt: iso(r.finishedAt),
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
return items;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* The LATEST run per PR for a batch of PR ids — the Open PRs table's "Claude review" column, ONE
|
|
267
|
+
* query for the whole list. Account-scoped through `repos.accountId` (a foreign or unknown id is
|
|
268
|
+
* simply absent). `headMoved` compares the run's head with the PR's SYNCED head; an unknown synced
|
|
269
|
+
* head is never "moved". DB-only: no GitHub call, no model.
|
|
270
|
+
*/
|
|
271
|
+
export async function getLatestReviewStates(ctx, prIds, accountId,
|
|
272
|
+
// TRAILING: the AI Fix manager's in-memory claim (`isFixRunning`). When given, each state also
|
|
273
|
+
// carries `fix` ('running' / 'ready') from ONE batched ai_fixes read. Absent ⇒ no `fix` field.
|
|
274
|
+
fixRunning) {
|
|
275
|
+
if (prIds.length === 0)
|
|
276
|
+
return [];
|
|
277
|
+
const { cr, prs, repos } = tables(ctx);
|
|
278
|
+
const rows = (await ctx.db
|
|
279
|
+
.select({
|
|
280
|
+
reviewId: cr.id,
|
|
281
|
+
prId: cr.prId,
|
|
282
|
+
status: cr.status,
|
|
283
|
+
verdict: cr.verdict,
|
|
284
|
+
headSha: cr.headSha,
|
|
285
|
+
finishedAt: cr.finishedAt,
|
|
286
|
+
ticket: cr.ticket,
|
|
287
|
+
prHeadSha: prs.headSha,
|
|
288
|
+
trigger: cr.trigger,
|
|
289
|
+
})
|
|
290
|
+
.from(cr)
|
|
291
|
+
.innerJoin(prs, eq(prs.id, cr.prId))
|
|
292
|
+
.innerJoin(repos, eq(repos.id, prs.repoId))
|
|
293
|
+
.where(and(inArray(cr.prId, [...prIds]), eq(repos.accountId, accountId)))
|
|
294
|
+
.orderBy(desc(cr.id))
|
|
295
|
+
.execute());
|
|
296
|
+
const out = [];
|
|
297
|
+
const latestByPr = new Map();
|
|
298
|
+
for (const r of rows) {
|
|
299
|
+
if (latestByPr.has(r.prId))
|
|
300
|
+
continue; // newest first — the first row per PR is its latest run
|
|
301
|
+
latestByPr.set(r.prId, r);
|
|
302
|
+
out.push({
|
|
303
|
+
prId: r.prId,
|
|
304
|
+
reviewId: r.reviewId,
|
|
305
|
+
status: r.status,
|
|
306
|
+
verdict: r.verdict,
|
|
307
|
+
reviewedHeadSha: r.headSha,
|
|
308
|
+
finishedAt: iso(r.finishedAt),
|
|
309
|
+
ticket: storedList(r.ticket)[0] ?? null,
|
|
310
|
+
tickets: storedList(r.ticket),
|
|
311
|
+
headMoved: r.prHeadSha != null && r.prHeadSha !== r.headSha,
|
|
312
|
+
currentHeadSha: r.prHeadSha ?? null,
|
|
313
|
+
trigger: r.trigger === 'auto' ? 'auto' : 'manual',
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
await foldStateSummaries(ctx, out, latestByPr);
|
|
317
|
+
if (fixRunning)
|
|
318
|
+
await foldFixStates(ctx, out, latestByPr, accountId, fixRunning);
|
|
319
|
+
return out;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* The Open PRs strip's AI Fix pill, for the listed PRs in ONE read of `ai_fixes`: `'running'` while
|
|
323
|
+
* the fixer holds a queued / running fix for the PR (the manager's claim is the authority — a row
|
|
324
|
+
* left `running` by a crash is not), else `'ready'` when a SUCCEEDED fix with a non-empty patch,
|
|
325
|
+
* never pushed, was built on the PR's CURRENT synced head. Mutates `states` in place.
|
|
326
|
+
*/
|
|
327
|
+
async function foldFixStates(ctx, states, latestByPr, accountId, fixRunning) {
|
|
328
|
+
if (states.length === 0)
|
|
329
|
+
return;
|
|
330
|
+
const af = ctx.schema.aiFixes;
|
|
331
|
+
const rows = (await ctx.db
|
|
332
|
+
.select({ prId: af.prId, status: af.status, baseSha: af.baseSha, patch: af.patch, pushedAt: af.pushedAt })
|
|
333
|
+
.from(af)
|
|
334
|
+
.where(and(eq(af.accountId, accountId), inArray(af.prId, states.map((s) => s.prId))))
|
|
335
|
+
.execute());
|
|
336
|
+
const running = new Set();
|
|
337
|
+
const ready = new Set();
|
|
338
|
+
for (const r of rows) {
|
|
339
|
+
if ((r.status === 'queued' || r.status === 'running') && fixRunning(r.prId))
|
|
340
|
+
running.add(r.prId);
|
|
341
|
+
const head = latestByPr.get(r.prId)?.prHeadSha ?? null;
|
|
342
|
+
if (r.status === 'succeeded' &&
|
|
343
|
+
r.pushedAt == null &&
|
|
344
|
+
(r.patch ?? '').trim() !== '' &&
|
|
345
|
+
head != null &&
|
|
346
|
+
r.baseSha === head)
|
|
347
|
+
ready.add(r.prId);
|
|
348
|
+
}
|
|
349
|
+
for (const s of states) {
|
|
350
|
+
if (running.has(s.prId))
|
|
351
|
+
s.fix = 'running';
|
|
352
|
+
else if (ready.has(s.prId))
|
|
353
|
+
s.fix = 'ready';
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* The Open PRs strip's figures for the latest runs: severity / lens / posted counts, story
|
|
358
|
+
* alignments, follow-up statuses and the outdated commit count. A FIXED number of batched reads
|
|
359
|
+
* (findings, the run's JSON columns, the moved PRs' commits) whatever the list length — never one
|
|
360
|
+
* per PR. Mutates `states` in place. A run that did not succeed gets no `summary`.
|
|
361
|
+
*/
|
|
362
|
+
async function foldStateSummaries(ctx, states, latestByPr) {
|
|
363
|
+
const { cr, crf } = tables(ctx);
|
|
364
|
+
const done = states.filter((s) => s.status === 'succeeded' && s.reviewId != null);
|
|
365
|
+
const doneIds = done.map((s) => s.reviewId);
|
|
366
|
+
// 1. Every finding of the finished runs — the columns the fold needs, nothing else.
|
|
367
|
+
const findings = doneIds.length
|
|
368
|
+
? (await ctx.db
|
|
369
|
+
.select({ reviewId: crf.reviewId, severity: crf.severity, lens: crf.lens, postedAt: crf.postedAt })
|
|
370
|
+
.from(crf)
|
|
371
|
+
.where(inArray(crf.reviewId, doneIds))
|
|
372
|
+
.execute())
|
|
373
|
+
: [];
|
|
374
|
+
// 2. The finished runs' stored assessments (JSON) — read for the LATEST runs only, never history.
|
|
375
|
+
const extras = doneIds.length
|
|
376
|
+
? (await ctx.db
|
|
377
|
+
.select({
|
|
378
|
+
id: cr.id,
|
|
379
|
+
ticketAssessment: cr.ticketAssessment,
|
|
380
|
+
followUp: cr.followUp,
|
|
381
|
+
postedAt: cr.postedAt,
|
|
382
|
+
threadAssessments: cr.threadAssessments,
|
|
383
|
+
ciFailures: cr.ciFailures,
|
|
384
|
+
})
|
|
385
|
+
.from(cr)
|
|
386
|
+
.where(inArray(cr.id, doneIds))
|
|
387
|
+
.execute())
|
|
388
|
+
: [];
|
|
389
|
+
const extrasById = new Map(extras.map((e) => [e.id, e]));
|
|
390
|
+
const findingsById = new Map();
|
|
391
|
+
for (const f of findings) {
|
|
392
|
+
const list = findingsById.get(f.reviewId) ?? [];
|
|
393
|
+
list.push(f);
|
|
394
|
+
findingsById.set(f.reviewId, list);
|
|
395
|
+
}
|
|
396
|
+
for (const s of done) {
|
|
397
|
+
const id = s.reviewId;
|
|
398
|
+
const counts = { blocker: 0, warning: 0, nit: 0, question: 0, praise: 0 };
|
|
399
|
+
const lenses = {};
|
|
400
|
+
let postedFindings = 0;
|
|
401
|
+
for (const f of findingsById.get(id) ?? []) {
|
|
402
|
+
if (f.severity in counts)
|
|
403
|
+
counts[f.severity] += 1;
|
|
404
|
+
const lens = CLAUDE_FINDING_LENSES.includes(f.lens ?? '') ? f.lens : null;
|
|
405
|
+
if (lens)
|
|
406
|
+
lenses[lens] = (lenses[lens] ?? 0) + 1;
|
|
407
|
+
if (f.postedAt != null)
|
|
408
|
+
postedFindings += 1;
|
|
409
|
+
}
|
|
410
|
+
const extra = extrasById.get(id);
|
|
411
|
+
const latest = latestByPr.get(s.prId);
|
|
412
|
+
const tickets = ticketEntriesOf((latest?.ticket ?? null), extra?.ticketAssessment ?? null).map((e) => ({
|
|
413
|
+
key: e.ticket.key ?? null,
|
|
414
|
+
title: e.ticket.title ?? null,
|
|
415
|
+
alignment: e.assessment?.alignment ?? null,
|
|
416
|
+
}));
|
|
417
|
+
let followUp = null;
|
|
418
|
+
const items = extra?.followUp?.items;
|
|
419
|
+
if (Array.isArray(items) && items.length > 0) {
|
|
420
|
+
followUp = { addressed: 0, partly_addressed: 0, not_addressed: 0, no_longer_applies: 0, not_checked: 0 };
|
|
421
|
+
for (const it of items)
|
|
422
|
+
if (it.status in followUp)
|
|
423
|
+
followUp[it.status] += 1;
|
|
424
|
+
}
|
|
425
|
+
const summary = {
|
|
426
|
+
findings: counts,
|
|
427
|
+
lenses,
|
|
428
|
+
postedFindings,
|
|
429
|
+
reviewPosted: extra?.postedAt != null,
|
|
430
|
+
tickets,
|
|
431
|
+
followUp,
|
|
432
|
+
};
|
|
433
|
+
// null = the run did not assess threads (an older row): no figure, never zeros.
|
|
434
|
+
if (Array.isArray(extra?.threadAssessments)) {
|
|
435
|
+
summary.threadAssessments = threadAssessmentCounts(extra.threadAssessments);
|
|
436
|
+
}
|
|
437
|
+
// null = the run did not look at CI: no figure, never zeros.
|
|
438
|
+
const ci = ciRecordOf(extra?.ciFailures);
|
|
439
|
+
if (ci) {
|
|
440
|
+
summary.ci = {
|
|
441
|
+
failing: ci.failures.length,
|
|
442
|
+
diagnosed: ci.failures.filter((f) => f.status === 'diagnosed').length,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
s.summary = summary;
|
|
446
|
+
}
|
|
447
|
+
// 3. The outdated count, for every moved PR — ONE commits read for all of them.
|
|
448
|
+
const moved = states.filter((s) => s.headMoved && s.reviewedHeadSha != null);
|
|
449
|
+
const c = ctx.schema.commits;
|
|
450
|
+
if (moved.length === 0 || !c)
|
|
451
|
+
return;
|
|
452
|
+
const commitRows = (await ctx.db
|
|
453
|
+
.select({ prId: c.prId, sha: c.sha, committedAt: c.committedAt, headline: c.messageHeadline })
|
|
454
|
+
.from(c)
|
|
455
|
+
.where(inArray(c.prId, moved.map((s) => s.prId)))
|
|
456
|
+
.execute());
|
|
457
|
+
const commitsByPr = new Map();
|
|
458
|
+
for (const r of commitRows) {
|
|
459
|
+
const list = commitsByPr.get(r.prId) ?? [];
|
|
460
|
+
list.push(r);
|
|
461
|
+
commitsByPr.set(r.prId, list);
|
|
462
|
+
}
|
|
463
|
+
for (const s of moved) {
|
|
464
|
+
const head = latestByPr.get(s.prId)?.prHeadSha;
|
|
465
|
+
if (!head)
|
|
466
|
+
continue;
|
|
467
|
+
s.commitsSince = countCommitsSince(commitsByPr.get(s.prId) ?? [], s.reviewedHeadSha, head);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
export async function getReviewPostContext(ctx, reviewId, accountId) {
|
|
471
|
+
const { cr, prs, repos } = tables(ctx);
|
|
472
|
+
const rows = (await ctx.db
|
|
473
|
+
.select({
|
|
474
|
+
headSha: cr.headSha,
|
|
475
|
+
userBody: cr.userBody,
|
|
476
|
+
owner: repos.owner,
|
|
477
|
+
name: repos.name,
|
|
478
|
+
prNumber: prs.number,
|
|
479
|
+
})
|
|
480
|
+
.from(cr)
|
|
481
|
+
.innerJoin(prs, eq(prs.id, cr.prId))
|
|
482
|
+
.innerJoin(repos, eq(repos.id, prs.repoId))
|
|
483
|
+
.where(and(eq(cr.id, reviewId), eq(repos.accountId, accountId)))
|
|
484
|
+
.limit(1)
|
|
485
|
+
.execute());
|
|
486
|
+
const row = rows[0];
|
|
487
|
+
if (!row)
|
|
488
|
+
return null;
|
|
489
|
+
return {
|
|
490
|
+
reviewHeadSha: row.headSha,
|
|
491
|
+
userBody: row.userBody,
|
|
492
|
+
owner: row.owner,
|
|
493
|
+
name: row.name,
|
|
494
|
+
prNumber: row.prNumber,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
export async function getFindingPostContext(ctx, findingId, accountId) {
|
|
498
|
+
const { cr, crf, prs, repos } = tables(ctx);
|
|
499
|
+
const rows = (await ctx.db
|
|
500
|
+
.select({
|
|
501
|
+
finding: crf,
|
|
502
|
+
reviewHeadSha: cr.headSha,
|
|
503
|
+
owner: repos.owner,
|
|
504
|
+
name: repos.name,
|
|
505
|
+
prNumber: prs.number,
|
|
506
|
+
})
|
|
507
|
+
.from(crf)
|
|
508
|
+
.innerJoin(cr, eq(cr.id, crf.reviewId))
|
|
509
|
+
.innerJoin(prs, eq(prs.id, cr.prId))
|
|
510
|
+
.innerJoin(repos, eq(repos.id, prs.repoId))
|
|
511
|
+
.where(and(eq(crf.id, findingId), eq(repos.accountId, accountId)))
|
|
512
|
+
.limit(1)
|
|
513
|
+
.execute());
|
|
514
|
+
const row = rows[0];
|
|
515
|
+
if (!row)
|
|
516
|
+
return null;
|
|
517
|
+
return { finding: row.finding, reviewHeadSha: row.reviewHeadSha, owner: row.owner, name: row.name, prNumber: row.prNumber };
|
|
518
|
+
}
|
|
519
|
+
export async function getReviewPrContext(ctx, prId, accountId) {
|
|
520
|
+
const { prs, repos } = tables(ctx);
|
|
521
|
+
const rows = (await ctx.db
|
|
522
|
+
.select({
|
|
523
|
+
prId: prs.id,
|
|
524
|
+
owner: repos.owner,
|
|
525
|
+
name: repos.name,
|
|
526
|
+
number: prs.number,
|
|
527
|
+
title: prs.title,
|
|
528
|
+
body: prs.body,
|
|
529
|
+
baseRefName: prs.baseRefName,
|
|
530
|
+
headSha: prs.headSha,
|
|
531
|
+
})
|
|
532
|
+
.from(prs)
|
|
533
|
+
.innerJoin(repos, eq(repos.id, prs.repoId))
|
|
534
|
+
.where(and(eq(prs.id, prId), eq(repos.accountId, accountId)))
|
|
535
|
+
.limit(1)
|
|
536
|
+
.execute());
|
|
537
|
+
const row = rows[0];
|
|
538
|
+
if (!row)
|
|
539
|
+
return null;
|
|
540
|
+
return {
|
|
541
|
+
prId: row.prId,
|
|
542
|
+
owner: row.owner,
|
|
543
|
+
name: row.name,
|
|
544
|
+
repoFullName: `${row.owner}/${row.name}`,
|
|
545
|
+
number: row.number,
|
|
546
|
+
title: row.title,
|
|
547
|
+
body: row.body,
|
|
548
|
+
baseRefName: row.baseRefName,
|
|
549
|
+
headSha: row.headSha,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
// ---- The previous review, for the follow-up ----
|
|
553
|
+
/**
|
|
554
|
+
* The PREVIOUS review a new run follows up on, with its eligible findings — or null.
|
|
555
|
+
*
|
|
556
|
+
* The previous review is the newest SUCCEEDED run for the same PR (account-scoped through the
|
|
557
|
+
* repos join — the getLatestClaudeReview IDOR pattern) with an id below the current run's, that
|
|
558
|
+
* actually READ code: `review_mode IS NULL OR review_mode <> 'skip'`. ⚠ The NULL arm is
|
|
559
|
+
* load-bearing — pre-routing rows have no mode, and a bare `<>` drops NULL rows in SQL.
|
|
560
|
+
*
|
|
561
|
+
* Its findings are kept when eligible (`isFollowUpEligible`: POSTED to GitHub and not praise),
|
|
562
|
+
* with the body the user saw. A finding that was ignored, left unposted or only copied never enters
|
|
563
|
+
* the follow-up — not the prompt, not the stored record, not the counts. CARRY-FORWARD re-loads two kinds of OLDER finding named by
|
|
564
|
+
* that run's follow-up items — the ids come only from our own stored JSON and are re-scoped to this
|
|
565
|
+
* PR + account by the query — and marks them `carried`, so nothing drops out of the chain:
|
|
566
|
+
* - every 'not_checked' item (never shown to the model, or not reported on), when eligible;
|
|
567
|
+
* - every still-open item (not / partly addressed) that no ELIGIBLE (posted) finding of that
|
|
568
|
+
* run raises again, when the earlier finding is itself eligible. That keeps a posted comment in
|
|
569
|
+
* the chain when its re-raise was never posted — the re-raise follow-up.ts saved left out
|
|
570
|
+
* because the comment was already on the same commit (`isAlreadyOnThisCommit`), or one the
|
|
571
|
+
* reader simply did not post — so it is followed up against the comment that IS on the PR.
|
|
572
|
+
*
|
|
573
|
+
* Every finding carries the head of the review that RAISED it (`cr.headSha` via the join), because
|
|
574
|
+
* "has the code moved since?" is asked per finding, never against the previous review's head.
|
|
575
|
+
*/
|
|
576
|
+
export async function loadPriorReviewForFollowUp(ctx, prId, accountId, beforeReviewId) {
|
|
577
|
+
const { cr, crf, prs, repos } = tables(ctx);
|
|
578
|
+
const rows = (await ctx.db
|
|
579
|
+
.select({ id: cr.id, headSha: cr.headSha, followUp: cr.followUp })
|
|
580
|
+
.from(cr)
|
|
581
|
+
.innerJoin(prs, eq(prs.id, cr.prId))
|
|
582
|
+
.innerJoin(repos, eq(repos.id, prs.repoId))
|
|
583
|
+
.where(and(eq(cr.prId, prId), eq(repos.accountId, accountId), eq(cr.status, 'succeeded'), lt(cr.id, beforeReviewId), or(isNull(cr.reviewMode), ne(cr.reviewMode, 'skip'))))
|
|
584
|
+
.orderBy(desc(cr.id))
|
|
585
|
+
.limit(1)
|
|
586
|
+
.execute());
|
|
587
|
+
const row = rows[0];
|
|
588
|
+
if (!row)
|
|
589
|
+
return null;
|
|
590
|
+
const toPrior = (f, headSha, carried) => ({
|
|
591
|
+
id: f.id,
|
|
592
|
+
headSha,
|
|
593
|
+
path: f.path,
|
|
594
|
+
line: f.line,
|
|
595
|
+
side: f.side,
|
|
596
|
+
severity: f.severity,
|
|
597
|
+
title: f.title,
|
|
598
|
+
body: resolveFindingBody(f),
|
|
599
|
+
suggestion: f.suggestion,
|
|
600
|
+
diffHunk: f.diffHunk,
|
|
601
|
+
anchored: f.anchored,
|
|
602
|
+
fileInDiff: f.fileInDiff,
|
|
603
|
+
posted: f.postedAt != null,
|
|
604
|
+
carried,
|
|
605
|
+
story: storyOf(f),
|
|
606
|
+
});
|
|
607
|
+
const own = (await ctx.db
|
|
608
|
+
.select()
|
|
609
|
+
.from(crf)
|
|
610
|
+
.where(eq(crf.reviewId, row.id))
|
|
611
|
+
.orderBy(asc(crf.id))
|
|
612
|
+
.execute());
|
|
613
|
+
const eligibleOwn = own.filter(isFollowUpEligible);
|
|
614
|
+
const findings = eligibleOwn.map((f) => toPrior(f, row.headSha, false));
|
|
615
|
+
const seen = new Set(own.map((f) => f.id));
|
|
616
|
+
// An earlier finding an eligible finding of that run already raises again is followed up
|
|
617
|
+
// THROUGH that re-raise, never twice.
|
|
618
|
+
const reraisedByEligible = new Set(eligibleOwn.map((f) => f.priorFindingId).filter((id) => id != null));
|
|
619
|
+
const notChecked = new Set();
|
|
620
|
+
const openNotReraised = new Map();
|
|
621
|
+
for (const it of row.followUp?.items ?? []) {
|
|
622
|
+
if (!Number.isInteger(it.priorFindingId) || seen.has(it.priorFindingId))
|
|
623
|
+
continue;
|
|
624
|
+
if (it.status === 'not_checked')
|
|
625
|
+
notChecked.add(it.priorFindingId);
|
|
626
|
+
else if ((it.status === 'not_addressed' || it.status === 'partly_addressed') &&
|
|
627
|
+
!reraisedByEligible.has(it.priorFindingId)) {
|
|
628
|
+
// Kept with the answer that run gave: on a same-head re-run it stands (follow-up.ts).
|
|
629
|
+
openNotReraised.set(it.priorFindingId, { status: it.status, explanation: it.explanation ?? null });
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
const carriedIds = [...new Set([...notChecked, ...openNotReraised.keys()])];
|
|
633
|
+
if (carriedIds.length > 0) {
|
|
634
|
+
const carriedRows = (await ctx.db
|
|
635
|
+
.select({ finding: crf, headSha: cr.headSha })
|
|
636
|
+
.from(crf)
|
|
637
|
+
.innerJoin(cr, eq(cr.id, crf.reviewId))
|
|
638
|
+
.innerJoin(prs, eq(prs.id, cr.prId))
|
|
639
|
+
.innerJoin(repos, eq(repos.id, prs.repoId))
|
|
640
|
+
.where(and(inArray(crf.id, carriedIds), eq(cr.prId, prId), eq(repos.accountId, accountId)))
|
|
641
|
+
.orderBy(asc(crf.id))
|
|
642
|
+
.execute());
|
|
643
|
+
for (const { finding, headSha } of carriedRows) {
|
|
644
|
+
// The ONE rule (posted, not praise) applies to carried findings exactly as to own ones.
|
|
645
|
+
if (seen.has(finding.id) || !isFollowUpEligible(finding))
|
|
646
|
+
continue;
|
|
647
|
+
seen.add(finding.id);
|
|
648
|
+
findings.push({ ...toPrior(finding, headSha, true), priorStatus: openNotReraised.get(finding.id) ?? null });
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return { reviewId: row.id, headSha: row.headSha, findings };
|
|
652
|
+
}
|
|
653
|
+
// ---- Writers ----
|
|
654
|
+
// The tickets are stored at QUEUE time (not on success), so a failed or cancelled run still
|
|
655
|
+
// prefills the SPA's panel for the re-run. Stored as an ARRAY (null when none).
|
|
656
|
+
export async function insertQueuedReview(ctx, prId, headSha, model, accountId, tickets = [],
|
|
657
|
+
// TRAILING: who started it. 'auto' = the per-workspace sweeper (auto.ts). A label only: core's
|
|
658
|
+
// My Turn read keeps an auto run exactly like a manual one, and the chip says "Auto review".
|
|
659
|
+
trigger = 'manual') {
|
|
660
|
+
const { cr } = tables(ctx);
|
|
661
|
+
const rows = (await ctx.db
|
|
662
|
+
.insert(cr)
|
|
663
|
+
.values({
|
|
664
|
+
accountId,
|
|
665
|
+
prId,
|
|
666
|
+
headSha,
|
|
667
|
+
status: 'queued',
|
|
668
|
+
model,
|
|
669
|
+
ticket: tickets.length > 0 ? [...tickets] : null,
|
|
670
|
+
trigger,
|
|
671
|
+
})
|
|
672
|
+
.returning({ id: cr.id })
|
|
673
|
+
.execute());
|
|
674
|
+
return rows[0].id;
|
|
675
|
+
}
|
|
676
|
+
export async function markReviewRunning(ctx, id) {
|
|
677
|
+
const { cr } = tables(ctx);
|
|
678
|
+
await ctx.db.update(cr).set({ status: 'running' }).where(eq(cr.id, id)).execute();
|
|
679
|
+
}
|
|
680
|
+
export async function markReviewRouted(ctx, id, reviewMode, routeReason) {
|
|
681
|
+
const { cr } = tables(ctx);
|
|
682
|
+
await ctx.db.update(cr).set({ reviewMode, routeReason }).where(eq(cr.id, id)).execute();
|
|
683
|
+
}
|
|
684
|
+
export async function saveReviewSuccess(ctx, id, data) {
|
|
685
|
+
const { cr, crf } = tables(ctx);
|
|
686
|
+
await ctx.runTransaction(async (tx) => {
|
|
687
|
+
await tx
|
|
688
|
+
.update(cr)
|
|
689
|
+
.set({
|
|
690
|
+
status: 'succeeded',
|
|
691
|
+
scope: data.scope,
|
|
692
|
+
summary: data.summary,
|
|
693
|
+
verdict: data.verdict,
|
|
694
|
+
costUsd: data.costUsd,
|
|
695
|
+
inputTokens: data.inputTokens,
|
|
696
|
+
outputTokens: data.outputTokens,
|
|
697
|
+
cacheReadTokens: data.cacheReadTokens ?? null,
|
|
698
|
+
cacheCreationTokens: data.cacheCreationTokens ?? null,
|
|
699
|
+
numTurns: data.numTurns,
|
|
700
|
+
diffBytes: data.diffBytes ?? null,
|
|
701
|
+
diffCapped: data.diffCapped ?? null,
|
|
702
|
+
excludedFiles: data.excludedFiles,
|
|
703
|
+
followUp: data.followUp ?? null,
|
|
704
|
+
ticketAssessment: data.ticketAssessment ?? null,
|
|
705
|
+
threadAssessments: data.threadAssessments ?? null,
|
|
706
|
+
ciFailures: data.ciFailures ?? null,
|
|
707
|
+
finishedAt: new Date(),
|
|
708
|
+
})
|
|
709
|
+
.where(eq(cr.id, id))
|
|
710
|
+
.execute();
|
|
711
|
+
for (const f of data.findings) {
|
|
712
|
+
await tx
|
|
713
|
+
.insert(crf)
|
|
714
|
+
.values({
|
|
715
|
+
reviewId: id,
|
|
716
|
+
path: f.path,
|
|
717
|
+
line: f.line,
|
|
718
|
+
side: f.side,
|
|
719
|
+
severity: f.severity,
|
|
720
|
+
title: f.title,
|
|
721
|
+
body: f.body,
|
|
722
|
+
suggestion: f.suggestion,
|
|
723
|
+
diffHunk: f.diffHunk,
|
|
724
|
+
anchored: f.anchored,
|
|
725
|
+
fileInDiff: f.fileInDiff,
|
|
726
|
+
// Findings are INCLUDED by default (the UI is opt-OUT); the column default is
|
|
727
|
+
// false for back-compat, so set it here. The one exception arrives as `included:
|
|
728
|
+
// false`: a re-raise of a comment already posted on this same commit (follow-up.ts
|
|
729
|
+
// `isAlreadyOnThisCommit`), so Post review does not put it on GitHub twice.
|
|
730
|
+
included: f.included ?? true,
|
|
731
|
+
priorFindingId: f.priorFindingId ?? null,
|
|
732
|
+
lens: f.lens ?? null,
|
|
733
|
+
// Set only on a story finding (ticket.ts `storyFindingsFrom`, or a re-raise of one).
|
|
734
|
+
storyIndex: f.story?.index ?? null,
|
|
735
|
+
storyRef: f.story?.ref ?? null,
|
|
736
|
+
})
|
|
737
|
+
.execute();
|
|
738
|
+
}
|
|
739
|
+
});
|
|
740
|
+
await recordReviewUsage(ctx, id, data.costUsd, data.inputTokens, data.outputTokens).catch(() => { });
|
|
741
|
+
}
|
|
742
|
+
export async function markReviewFailed(ctx, id, error, telemetry = {}) {
|
|
743
|
+
const { cr } = tables(ctx);
|
|
744
|
+
await ctx.db
|
|
745
|
+
.update(cr)
|
|
746
|
+
.set({
|
|
747
|
+
status: 'failed',
|
|
748
|
+
error: error.slice(0, 4000),
|
|
749
|
+
costUsd: telemetry.costUsd ?? null,
|
|
750
|
+
inputTokens: telemetry.inputTokens ?? null,
|
|
751
|
+
outputTokens: telemetry.outputTokens ?? null,
|
|
752
|
+
cacheReadTokens: telemetry.cacheReadTokens ?? null,
|
|
753
|
+
cacheCreationTokens: telemetry.cacheCreationTokens ?? null,
|
|
754
|
+
numTurns: telemetry.numTurns ?? null,
|
|
755
|
+
diffBytes: telemetry.diffBytes ?? null,
|
|
756
|
+
diffCapped: telemetry.diffCapped ?? null,
|
|
757
|
+
scope: telemetry.scope ?? null,
|
|
758
|
+
excludedFiles: telemetry.excludedFiles ?? null,
|
|
759
|
+
finishedAt: new Date(),
|
|
760
|
+
})
|
|
761
|
+
.where(eq(cr.id, id))
|
|
762
|
+
.execute();
|
|
763
|
+
await recordReviewUsage(ctx, id, telemetry.costUsd, telemetry.inputTokens, telemetry.outputTokens).catch(() => { });
|
|
764
|
+
}
|
|
765
|
+
export async function markReviewCancelled(ctx, id) {
|
|
766
|
+
const { cr } = tables(ctx);
|
|
767
|
+
await ctx.db
|
|
768
|
+
.update(cr)
|
|
769
|
+
.set({ status: 'cancelled', finishedAt: new Date() })
|
|
770
|
+
.where(eq(cr.id, id))
|
|
771
|
+
.execute();
|
|
772
|
+
}
|
|
773
|
+
export async function updateReviewDraft(ctx, id, fields) {
|
|
774
|
+
const set = {
|
|
775
|
+
...(fields.userBody !== undefined ? { userBody: fields.userBody } : {}),
|
|
776
|
+
...(fields.userVerdict !== undefined ? { userVerdict: fields.userVerdict } : {}),
|
|
777
|
+
};
|
|
778
|
+
if (Object.keys(set).length === 0)
|
|
779
|
+
return true;
|
|
780
|
+
const { cr } = tables(ctx);
|
|
781
|
+
const changed = (await ctx.db
|
|
782
|
+
.update(cr)
|
|
783
|
+
.set(set)
|
|
784
|
+
.where(eq(cr.id, id))
|
|
785
|
+
.returning({ id: cr.id })
|
|
786
|
+
.execute());
|
|
787
|
+
return changed.length > 0;
|
|
788
|
+
}
|
|
789
|
+
export async function updateFinding(ctx, findingId, fields) {
|
|
790
|
+
const set = {
|
|
791
|
+
...(fields.included !== undefined ? { included: fields.included } : {}),
|
|
792
|
+
...(fields.editedBody !== undefined
|
|
793
|
+
? { editedBody: fields.editedBody === '' ? null : fields.editedBody }
|
|
794
|
+
: {}),
|
|
795
|
+
};
|
|
796
|
+
if (Object.keys(set).length === 0)
|
|
797
|
+
return true;
|
|
798
|
+
const { crf } = tables(ctx);
|
|
799
|
+
const changed = (await ctx.db
|
|
800
|
+
.update(crf)
|
|
801
|
+
.set(set)
|
|
802
|
+
.where(eq(crf.id, findingId))
|
|
803
|
+
.returning({ id: crf.id })
|
|
804
|
+
.execute());
|
|
805
|
+
return changed.length > 0;
|
|
806
|
+
}
|
|
807
|
+
export async function markReviewPosted(ctx, id, postedReviewId, inlineFindingIds, prComments = []) {
|
|
808
|
+
const { cr, crf } = tables(ctx);
|
|
809
|
+
const now = new Date();
|
|
810
|
+
await ctx.runTransaction(async (tx) => {
|
|
811
|
+
await tx.update(cr).set({ postedReviewId, postedAt: now }).where(eq(cr.id, id)).execute();
|
|
812
|
+
if (inlineFindingIds.length > 0) {
|
|
813
|
+
await tx
|
|
814
|
+
.update(crf)
|
|
815
|
+
.set({ postedAt: now, postedCommentKind: 'inline' })
|
|
816
|
+
.where(inArray(crf.id, inlineFindingIds))
|
|
817
|
+
.execute();
|
|
818
|
+
}
|
|
819
|
+
for (const pc of prComments) {
|
|
820
|
+
await tx
|
|
821
|
+
.update(crf)
|
|
822
|
+
.set({ postedAt: now, githubCommentId: pc.commentId, postedCommentKind: 'pr_comment' })
|
|
823
|
+
.where(eq(crf.id, pc.findingId))
|
|
824
|
+
.execute();
|
|
825
|
+
}
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
export async function markFindingPosted(ctx, findingId, githubCommentId, kind = 'inline') {
|
|
829
|
+
const { crf } = tables(ctx);
|
|
830
|
+
await ctx.db
|
|
831
|
+
.update(crf)
|
|
832
|
+
.set({ postedAt: new Date(), githubCommentId, postedCommentKind: kind })
|
|
833
|
+
.where(eq(crf.id, findingId))
|
|
834
|
+
.execute();
|
|
835
|
+
}
|
|
836
|
+
// On startup, heal runs left mid-flight by a crash/restart.
|
|
837
|
+
export async function reconcileOrphanedReviews(ctx) {
|
|
838
|
+
const { cr } = tables(ctx);
|
|
839
|
+
const changed = (await ctx.db
|
|
840
|
+
.update(cr)
|
|
841
|
+
.set({ status: 'failed', error: 'interrupted by restart', finishedAt: new Date() })
|
|
842
|
+
.where(inArray(cr.status, ['queued', 'running']))
|
|
843
|
+
.returning({ id: cr.id })
|
|
844
|
+
.execute());
|
|
845
|
+
return changed.length;
|
|
846
|
+
}
|
|
847
|
+
// Record a run's cost on the shared AI-usage ledger (agent seam). Reads accountId/model/prId
|
|
848
|
+
// back off the row. Best-effort — a $0 / non-billing run records nothing.
|
|
849
|
+
async function recordReviewUsage(ctx, id, costUsd, inputTokens, outputTokens) {
|
|
850
|
+
if (costUsd == null || !Number.isFinite(costUsd) || costUsd <= 0)
|
|
851
|
+
return;
|
|
852
|
+
const { cr } = tables(ctx);
|
|
853
|
+
const row = (await ctx.db
|
|
854
|
+
.select({ accountId: cr.accountId, model: cr.model, prId: cr.prId })
|
|
855
|
+
.from(cr)
|
|
856
|
+
.where(eq(cr.id, id))
|
|
857
|
+
.limit(1)
|
|
858
|
+
.execute())[0];
|
|
859
|
+
if (!row)
|
|
860
|
+
return;
|
|
861
|
+
await ctx.recordAiUsage({
|
|
862
|
+
accountId: row.accountId,
|
|
863
|
+
seam: 'agent',
|
|
864
|
+
feature: 'claude_review',
|
|
865
|
+
model: row.model,
|
|
866
|
+
costUsd,
|
|
867
|
+
inputTokens: inputTokens ?? null,
|
|
868
|
+
outputTokens: outputTokens ?? null,
|
|
869
|
+
prId: row.prId,
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* The pure half of `reviewHeadState` (also folded per PR by the batched Open PRs states read):
|
|
874
|
+
* how many of a PR's synced commits are newer than the reviewed one. null when either commit is
|
|
875
|
+
* not synced, or the history looks rewritten (a newer copy with the reviewed commit's headline).
|
|
876
|
+
*/
|
|
877
|
+
export function countCommitsSince(rows, reviewedSha, currentHeadSha) {
|
|
878
|
+
const ms = (v) => (v instanceof Date ? v.getTime() : Number(v));
|
|
879
|
+
const reviewed = rows.find((r) => r.sha === reviewedSha);
|
|
880
|
+
const head = rows.find((r) => r.sha === currentHeadSha);
|
|
881
|
+
if (!reviewed || !head)
|
|
882
|
+
return null;
|
|
883
|
+
const at = ms(reviewed.committedAt);
|
|
884
|
+
const newer = rows.filter((r) => r.sha !== reviewedSha && ms(r.committedAt) > at);
|
|
885
|
+
// Sync never prunes commits a force-push dropped, so a rebase or amend leaves the reviewed
|
|
886
|
+
// commit's own rewritten copy among the "newer" rows (same headline, later time). Counting
|
|
887
|
+
// those would call a rewrite "N newer commits"; say only that the branch changed (null).
|
|
888
|
+
const rewritten = reviewed.headline != null && newer.some((r) => r.headline === reviewed.headline);
|
|
889
|
+
return rewritten ? null : newer.length;
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Is the review behind the PR's head? DB-only. `commitsSince` counts the PR's synced commits newer
|
|
893
|
+
* than the reviewed one (by commit time); null when the reviewed commit is not among them, or when
|
|
894
|
+
* it looks rewritten (a newer copy with its headline) — never a guess. null overall when the PR's head is unknown.
|
|
895
|
+
*/
|
|
896
|
+
export async function reviewHeadState(ctx, prId, reviewedSha, currentHeadSha) {
|
|
897
|
+
if (!currentHeadSha)
|
|
898
|
+
return null;
|
|
899
|
+
if (currentHeadSha === reviewedSha) {
|
|
900
|
+
return { currentHeadSha, outdated: false, commitsSince: 0 };
|
|
901
|
+
}
|
|
902
|
+
const c = ctx.schema.commits;
|
|
903
|
+
let commitsSince = null;
|
|
904
|
+
if (c) {
|
|
905
|
+
const rows = (await ctx.db
|
|
906
|
+
.select({ sha: c.sha, committedAt: c.committedAt, headline: c.messageHeadline })
|
|
907
|
+
.from(c)
|
|
908
|
+
.where(eq(c.prId, prId))
|
|
909
|
+
.execute());
|
|
910
|
+
commitsSince = countCommitsSince(rows, reviewedSha, currentHeadSha);
|
|
911
|
+
}
|
|
912
|
+
return { currentHeadSha, outdated: true, commitsSince };
|
|
913
|
+
}
|
|
914
|
+
// ---- Auto re-review helpers ----
|
|
915
|
+
/** Is there ANY run (any status) of this PR at `headSha`? One run per head. */
|
|
916
|
+
export async function hasReviewAtHead(ctx, prId, accountId, headSha) {
|
|
917
|
+
const { cr } = tables(ctx);
|
|
918
|
+
const rows = (await ctx.db
|
|
919
|
+
.select({ id: cr.id })
|
|
920
|
+
.from(cr)
|
|
921
|
+
.where(and(eq(cr.prId, prId), eq(cr.accountId, accountId), eq(cr.headSha, headSha)))
|
|
922
|
+
.limit(1)
|
|
923
|
+
.execute());
|
|
924
|
+
return rows.length > 0;
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* Is the auto re-review keyed (headSha, commentsAtMs) already SETTLED? Any run of this PR at the
|
|
928
|
+
* head settles a moved head (one run per head). For new comments on an unchanged head
|
|
929
|
+
* (`commentsAtMs` set) a run at the head settles it only when it covered that comment: its
|
|
930
|
+
* `comments_through` (else its start, `created_at`) is at or after it. The sweeper's candidate read
|
|
931
|
+
* (db/queries.ts `getAutoReviewCandidates`) applies the same rule; this is the re-check a waiting
|
|
932
|
+
* item makes before its row is written.
|
|
933
|
+
*/
|
|
934
|
+
export async function isAutoReReviewSettled(ctx, prId, accountId, headSha, commentsAtMs) {
|
|
935
|
+
const { cr } = tables(ctx);
|
|
936
|
+
const rows = (await ctx.db
|
|
937
|
+
.select({ createdAt: cr.createdAt, commentsThrough: cr.commentsThrough })
|
|
938
|
+
.from(cr)
|
|
939
|
+
.where(and(eq(cr.prId, prId), eq(cr.accountId, accountId), eq(cr.headSha, headSha)))
|
|
940
|
+
.execute());
|
|
941
|
+
if (rows.length === 0)
|
|
942
|
+
return false;
|
|
943
|
+
if (commentsAtMs == null)
|
|
944
|
+
return true;
|
|
945
|
+
return rows.some((r) => (r.commentsThrough ?? r.createdAt).getTime() >= commentsAtMs);
|
|
946
|
+
}
|
|
947
|
+
/** Record the newest qualifying review comment the run saw (the comment half of the re-review key). */
|
|
948
|
+
export async function markReviewCommentsSeen(ctx, id, commentsThrough) {
|
|
949
|
+
if (!commentsThrough)
|
|
950
|
+
return;
|
|
951
|
+
const { cr } = tables(ctx);
|
|
952
|
+
await ctx.db.update(cr).set({ commentsThrough }).where(eq(cr.id, id)).execute();
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* The previous SUCCEEDED run (non-skip, id below this one) — what a same-head run carries forward:
|
|
956
|
+
* its head, its stories + their assessments, and its thread assessments. null when none.
|
|
957
|
+
*/
|
|
958
|
+
export async function loadPriorRunForCarry(ctx, prId, accountId, beforeReviewId) {
|
|
959
|
+
const { cr } = tables(ctx);
|
|
960
|
+
const rows = (await ctx.db
|
|
961
|
+
.select({
|
|
962
|
+
headSha: cr.headSha,
|
|
963
|
+
ticket: cr.ticket,
|
|
964
|
+
ticketAssessment: cr.ticketAssessment,
|
|
965
|
+
threadAssessments: cr.threadAssessments,
|
|
966
|
+
ciFailures: cr.ciFailures,
|
|
967
|
+
})
|
|
968
|
+
.from(cr)
|
|
969
|
+
.where(and(eq(cr.prId, prId), eq(cr.accountId, accountId), eq(cr.status, 'succeeded'), lt(cr.id, beforeReviewId), or(isNull(cr.reviewMode), ne(cr.reviewMode, 'skip'))))
|
|
970
|
+
.orderBy(desc(cr.id))
|
|
971
|
+
.limit(1)
|
|
972
|
+
.execute());
|
|
973
|
+
const r = rows[0];
|
|
974
|
+
if (!r)
|
|
975
|
+
return null;
|
|
976
|
+
return {
|
|
977
|
+
headSha: r.headSha,
|
|
978
|
+
tickets: storedList(r.ticket),
|
|
979
|
+
ticketAssessments: storedList(r.ticketAssessment),
|
|
980
|
+
threadAssessments: Array.isArray(r.threadAssessments) ? r.threadAssessments : null,
|
|
981
|
+
ciFailures: ciRecordOf(r.ciFailures)?.failures ?? null,
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
/** The stories stored on the PR's latest run ([] when none) — what a re-review carries forward. */
|
|
985
|
+
export async function getLatestStoredTickets(ctx, prId, accountId) {
|
|
986
|
+
const { cr } = tables(ctx);
|
|
987
|
+
const rows = (await ctx.db
|
|
988
|
+
.select({ ticket: cr.ticket })
|
|
989
|
+
.from(cr)
|
|
990
|
+
.where(and(eq(cr.prId, prId), eq(cr.accountId, accountId)))
|
|
991
|
+
.orderBy(desc(cr.id))
|
|
992
|
+
.limit(1)
|
|
993
|
+
.execute());
|
|
994
|
+
return storedList(rows[0]?.ticket ?? null);
|
|
995
|
+
}
|
|
996
|
+
//# sourceMappingURL=persist.js.map
|