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,532 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { config } from '../config.js';
|
|
3
|
+
import { conflictFileEntries } from './model.js';
|
|
4
|
+
/** At most one live session per `(accountId, prId)`. */
|
|
5
|
+
const sessions = new Map();
|
|
6
|
+
const keyOf = (accountId, prId) => `${accountId}:${prId}`;
|
|
7
|
+
/**
|
|
8
|
+
* Drop everything past its TTL, on every map operation.
|
|
9
|
+
*
|
|
10
|
+
* A lazy sweep rather than an interval: the map holds at most a handful of entries, and an
|
|
11
|
+
* unref-less timer in a module every route file imports is a process that will not exit.
|
|
12
|
+
*/
|
|
13
|
+
function sweep(now) {
|
|
14
|
+
for (const [key, rec] of sessions) {
|
|
15
|
+
if (rec.expiresAt > now)
|
|
16
|
+
continue;
|
|
17
|
+
// A job still running owns its record — reaping it would leave the job writing into a
|
|
18
|
+
// record nobody can read, and the SSE stream on the other end waiting forever.
|
|
19
|
+
if (rec.openRunning || rec.commitRunning)
|
|
20
|
+
continue;
|
|
21
|
+
endStream(rec);
|
|
22
|
+
sessions.delete(key);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* How long a record may live NO MATTER HOW OFTEN IT IS READ.
|
|
27
|
+
*
|
|
28
|
+
* ⚠ `touch()` ALONE BOUNDS NOTHING. Every manifest read touches, and the SPA polls the manifest
|
|
29
|
+
* for as long as the overlay is mounted (`useConflictSession` — the poll is the recovery channel
|
|
30
|
+
* after a proxy cuts the stream, so it cannot simply stop at `ready`). A tab left open overnight
|
|
31
|
+
* therefore pushed `expiresAt` out forever: the record stayed resident, `liveSessionIds()` kept
|
|
32
|
+
* reporting it to the janitor, and its two fetch refs pinned objects against every repack.
|
|
33
|
+
*
|
|
34
|
+
* So the TTL measures IDLENESS and this measures AGE, and a session hits whichever comes first.
|
|
35
|
+
* Losing one is cheap by construction (the header): the model is rebuilt against the shas as they
|
|
36
|
+
* are now, and the reader's decisions are in the SPA's own store under the pinned key.
|
|
37
|
+
*/
|
|
38
|
+
const MAX_SESSION_LIFETIME_MS = 4 * 60 * 60_000;
|
|
39
|
+
/** Push the expiry out. Called on every successful lookup: an open resolver in use is not idle. */
|
|
40
|
+
function touch(rec, now) {
|
|
41
|
+
rec.expiresAt = Math.min(now + config.conflictSessionTtlMs, rec.createdAt + MAX_SESSION_LIFETIME_MS);
|
|
42
|
+
}
|
|
43
|
+
/* ═════════════════════ how many jobs may run ═════════════════════ */
|
|
44
|
+
/**
|
|
45
|
+
* ⚠ TWO CAPS, BECAUSE THEY ANSWER TWO DIFFERENT QUESTIONS — and they refuse in two different
|
|
46
|
+
* sentences, because they are two different facts about the world.
|
|
47
|
+
*
|
|
48
|
+
* PER ACCOUNT — one at a time. A resolver is a clone, two fetches and a merge-tree; a person
|
|
49
|
+
* resolves one pull request at a time, and a second one from the same account is a second tab
|
|
50
|
+
* or a second click, not a second job. The reader is told about THEIR OWN work: "you're already
|
|
51
|
+
* resolving one".
|
|
52
|
+
*
|
|
53
|
+
* GLOBAL — the machine's ceiling. It protects disk and CPU, and it is the ONLY thing the
|
|
54
|
+
* per-account cap cannot bound once there are many accounts.
|
|
55
|
+
*
|
|
56
|
+
* ⚠ A TENANT IS NEVER TOLD ABOUT ANOTHER TENANT'S LOAD. The global refusal says the service is
|
|
57
|
+
* busy and stops; it names no count, no account and no pull request. The predecessor was a single
|
|
58
|
+
* process-global cap of 2 whose own comment reasoned from "the LOCAL mode's single account", so
|
|
59
|
+
* in cloud one tenant opening two resolvers told every other tenant that two pull requests were
|
|
60
|
+
* already being prepared — false about their work, and a disclosure about somebody else's.
|
|
61
|
+
*/
|
|
62
|
+
const MAX_JOBS_PER_ACCOUNT = 1;
|
|
63
|
+
const MAX_CONCURRENT_JOBS = 4;
|
|
64
|
+
/**
|
|
65
|
+
* ⚠ AND TWO MORE CAPS ON THE RECORDS THEMSELVES, WHICH ARE NOT THE SAME THING AS THE JOB CAPS
|
|
66
|
+
* ABOVE. A job ends in seconds; the RECORD it leaves behind holds a whole `ConflictModel` — the
|
|
67
|
+
* base/ours/theirs text of every region, up to `config.conflictMaxTotalBytes` (8 MiB) of it — and
|
|
68
|
+
* lives until the TTL or the lifetime ceiling reaps it. `MAX_JOBS_PER_ACCOUNT` serialises the
|
|
69
|
+
* builds and refuses NONE of them, so one account scripting an open per conflicted pull request
|
|
70
|
+
* retained one model per PR: hundreds of megabytes of string data in a process every other tenant
|
|
71
|
+
* shares, bounded by nothing. This is the bound.
|
|
72
|
+
*
|
|
73
|
+
* The arithmetic is deliberately conservative and the worst case is the one to read: 24 × 8 MiB.
|
|
74
|
+
* Real models are orders of magnitude smaller (a conflict is a few hunks of a few files), so the
|
|
75
|
+
* ceiling is reached by an adversary long before it is reached by readers.
|
|
76
|
+
*
|
|
77
|
+
* ⚠ EVICTION, NOT REFUSAL, AND ONLY OF A SETTLED RECORD. A running job owns its record (the same
|
|
78
|
+
* rule `sweep` follows). What an evicted reader loses is what a restart loses — the model, never
|
|
79
|
+
* the decisions — so refusing the open instead would protect nothing and block the work.
|
|
80
|
+
*/
|
|
81
|
+
const MAX_SESSIONS_PER_ACCOUNT = 3;
|
|
82
|
+
const MAX_TOTAL_SESSIONS = 24;
|
|
83
|
+
/** Running jobs, split into this account's and everyone's, in ONE pass over the map. */
|
|
84
|
+
function jobCounts(accountId) {
|
|
85
|
+
let mine = 0;
|
|
86
|
+
let total = 0;
|
|
87
|
+
for (const rec of sessions.values()) {
|
|
88
|
+
if (!rec.openRunning && !rec.commitRunning)
|
|
89
|
+
continue;
|
|
90
|
+
total += 1;
|
|
91
|
+
if (rec.accountId === accountId)
|
|
92
|
+
mine += 1;
|
|
93
|
+
}
|
|
94
|
+
return { mine, total };
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Make room for one more record: drop a SETTLED session — unwatched before watched, and within
|
|
98
|
+
* each the one read longest ago — from this account's own share first and then the process's,
|
|
99
|
+
* until both ceilings have room.
|
|
100
|
+
*
|
|
101
|
+
* ⚠ SYNCHRONOUS, like everything else in the claim window. `expiresAt` is the touch order (every
|
|
102
|
+
* touch sets it from the same TTL), so the smallest is the one read longest ago.
|
|
103
|
+
*/
|
|
104
|
+
function evictForNewSession(accountId) {
|
|
105
|
+
// ⚠ AN UNWATCHED RECORD GOES FIRST. A live subscriber is a tab with the overlay open on
|
|
106
|
+
// screen; an abandoned one is exactly what this cap exists to collect. Both are survivable
|
|
107
|
+
// (the client falls through to the poll, gets "no longer open" and offers Start again), but
|
|
108
|
+
// one of them interrupts somebody mid-resolution and the other does not.
|
|
109
|
+
const rank = (rec) => [
|
|
110
|
+
rec.subscribers.size > 0 ? 1 : 0,
|
|
111
|
+
rec.expiresAt,
|
|
112
|
+
];
|
|
113
|
+
const evictOldest = (of) => {
|
|
114
|
+
let oldest = null;
|
|
115
|
+
for (const rec of of) {
|
|
116
|
+
if (rec.openRunning || rec.commitRunning)
|
|
117
|
+
continue;
|
|
118
|
+
if (oldest == null) {
|
|
119
|
+
oldest = rec;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const [w, e] = rank(rec);
|
|
123
|
+
const [ow, oe] = rank(oldest);
|
|
124
|
+
if (w < ow || (w === ow && e < oe))
|
|
125
|
+
oldest = rec;
|
|
126
|
+
}
|
|
127
|
+
if (oldest == null)
|
|
128
|
+
return false;
|
|
129
|
+
endStream(oldest);
|
|
130
|
+
sessions.delete(keyOf(oldest.accountId, oldest.prId));
|
|
131
|
+
return true;
|
|
132
|
+
};
|
|
133
|
+
// This account's share first — a hoarder must never evict somebody else's work to make room
|
|
134
|
+
// for its own fourth session.
|
|
135
|
+
for (;;) {
|
|
136
|
+
const mine = [...sessions.values()].filter((r) => r.accountId === accountId);
|
|
137
|
+
if (mine.length < MAX_SESSIONS_PER_ACCOUNT)
|
|
138
|
+
break;
|
|
139
|
+
if (!evictOldest(mine))
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
for (;;) {
|
|
143
|
+
if (sessions.size < MAX_TOTAL_SESSIONS)
|
|
144
|
+
break;
|
|
145
|
+
if (!evictOldest([...sessions.values()]))
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Why this account may not start a job right now, or null.
|
|
151
|
+
*
|
|
152
|
+
* ⚠ CALLED INSIDE THE SYNCHRONOUS CLAIM WINDOW and it must stay synchronous: an `await` between
|
|
153
|
+
* this and the `openRunning = true` / `commitRunning = true` write is the gap in which two
|
|
154
|
+
* requests both pass the check (the AI in-flight-slot defect, one feature over).
|
|
155
|
+
*/
|
|
156
|
+
function capacityRefusal(accountId) {
|
|
157
|
+
if (shuttingDown)
|
|
158
|
+
return 'shutdown';
|
|
159
|
+
const { mine, total } = jobCounts(accountId);
|
|
160
|
+
if (mine >= MAX_JOBS_PER_ACCOUNT)
|
|
161
|
+
return 'account';
|
|
162
|
+
if (total >= MAX_CONCURRENT_JOBS)
|
|
163
|
+
return 'capacity';
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
/* ═════════════════════ shutdown ═════════════════════ */
|
|
167
|
+
/**
|
|
168
|
+
* Set by the SIGTERM handler (index.ts). From here on no NEW job may be claimed, while the ones
|
|
169
|
+
* already running are left alone to finish — a commit mid-push is a `git push` GitHub may
|
|
170
|
+
* already have accepted, and killing it is how a reader ends up with a landed commit and a
|
|
171
|
+
* screen that says it failed.
|
|
172
|
+
*
|
|
173
|
+
* ⚠ IT DOES NOT PRESERVE SESSIONS, AND MUST NOT GROW INTO SOMETHING THAT DOES. A restart loses
|
|
174
|
+
* every session, deliberately and correctly (see the header): the model is pinned to
|
|
175
|
+
* `(headSha, baseSha, modelHash)` and the reader's decisions live in the SPA's own store. What
|
|
176
|
+
* this buys is the IN-FLIGHT PUSH, nothing else.
|
|
177
|
+
*/
|
|
178
|
+
let shuttingDown = false;
|
|
179
|
+
export function beginShutdown() {
|
|
180
|
+
shuttingDown = true;
|
|
181
|
+
}
|
|
182
|
+
/** Jobs still running anywhere in the process — the SIGTERM handler's wait condition. */
|
|
183
|
+
export function runningJobCount() {
|
|
184
|
+
let n = 0;
|
|
185
|
+
for (const rec of sessions.values())
|
|
186
|
+
if (rec.openRunning || rec.commitRunning)
|
|
187
|
+
n += 1;
|
|
188
|
+
return n;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* The session ids the janitor must NOT collect refs for.
|
|
192
|
+
*
|
|
193
|
+
* ⚠ LIVE MEANS PRESENT IN THIS MAP, not "running". A settled session sitting at `ready` still
|
|
194
|
+
* owns `refs/pierre/conflict/<id>/{head,base}`, and its commit re-reads those refs minutes
|
|
195
|
+
* later — deleting them mid-resolution is how a commit lands against objects that are no longer
|
|
196
|
+
* reachable.
|
|
197
|
+
*
|
|
198
|
+
* ⚠ IT SWEEPS FIRST, AND THAT IS NOT AN OPTIMISATION. Every other sweep is request-driven
|
|
199
|
+
* (claim/peek/get/drop), and the case the janitor exists for — a reader who opens the resolver
|
|
200
|
+
* and closes the TAB, so the deferred DELETE never lands — is by definition the case where no
|
|
201
|
+
* further request arrives. Without this the expired record sat in the map, reported itself live
|
|
202
|
+
* on every tick, and its two refs were counted in `refsKept` and kept forever: the janitor
|
|
203
|
+
* scanned, repacked, and collected exactly the leak it was written to collect.
|
|
204
|
+
*/
|
|
205
|
+
export function liveSessionIds(now = Date.now()) {
|
|
206
|
+
sweep(now);
|
|
207
|
+
const ids = new Set();
|
|
208
|
+
for (const rec of sessions.values())
|
|
209
|
+
ids.add(rec.sessionId);
|
|
210
|
+
return ids;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Claim the open slot for one PR.
|
|
214
|
+
*
|
|
215
|
+
* ⚠ SYNCHRONOUS, START TO FINISH, and it must stay that way. The whole point is that two POSTs
|
|
216
|
+
* arriving in the same tick cannot both start a clone; an `await` anywhere between the lookup
|
|
217
|
+
* and the `openRunning = true` write re-opens exactly that gap (the `synthesis/routes.ts`
|
|
218
|
+
* in-flight-slot pattern).
|
|
219
|
+
*
|
|
220
|
+
* `restart` discards a settled session and builds against the CURRENT shas. It cannot discard a
|
|
221
|
+
* RUNNING one — there is nothing to cancel a build with, and abandoning it would leave a clone
|
|
222
|
+
* and two fetches running for a record nobody will read.
|
|
223
|
+
*/
|
|
224
|
+
export function claimSession(accountId, prId, opts, now = Date.now()) {
|
|
225
|
+
sweep(now);
|
|
226
|
+
const key = keyOf(accountId, prId);
|
|
227
|
+
const live = sessions.get(key) ?? null;
|
|
228
|
+
if (live && (live.openRunning || live.commitRunning)) {
|
|
229
|
+
// Re-attaching to a build already in flight is the right answer for a second tab; a
|
|
230
|
+
// restart on top of one is not, and neither is a second commit.
|
|
231
|
+
if (!opts.restart && live.openRunning) {
|
|
232
|
+
touch(live, now);
|
|
233
|
+
return { kind: 'reused', session: live };
|
|
234
|
+
}
|
|
235
|
+
return { kind: 'busy', session: live, reason: 'pr' };
|
|
236
|
+
}
|
|
237
|
+
if (live && !opts.restart) {
|
|
238
|
+
touch(live, now);
|
|
239
|
+
return { kind: 'reused', session: live };
|
|
240
|
+
}
|
|
241
|
+
if (live) {
|
|
242
|
+
endStream(live);
|
|
243
|
+
sessions.delete(key);
|
|
244
|
+
}
|
|
245
|
+
const refusal = capacityRefusal(accountId);
|
|
246
|
+
if (refusal)
|
|
247
|
+
return { kind: 'busy', session: null, reason: refusal };
|
|
248
|
+
// AFTER the refusal, so a claim that is going to be refused never costs somebody their model.
|
|
249
|
+
evictForNewSession(accountId);
|
|
250
|
+
const rec = {
|
|
251
|
+
sessionId: randomUUID(),
|
|
252
|
+
accountId,
|
|
253
|
+
prId,
|
|
254
|
+
status: 'preparing',
|
|
255
|
+
phase: null,
|
|
256
|
+
error: null,
|
|
257
|
+
autoApply: opts.autoApply,
|
|
258
|
+
model: null,
|
|
259
|
+
modelHash: '',
|
|
260
|
+
commit: null,
|
|
261
|
+
suggestions: new Map(),
|
|
262
|
+
edits: new Map(),
|
|
263
|
+
createdAt: now,
|
|
264
|
+
expiresAt: now + config.conflictSessionTtlMs,
|
|
265
|
+
openRunning: true,
|
|
266
|
+
commitRunning: false,
|
|
267
|
+
commitAbort: null,
|
|
268
|
+
subscribers: new Set(),
|
|
269
|
+
};
|
|
270
|
+
sessions.set(key, rec);
|
|
271
|
+
return { kind: 'created', session: rec };
|
|
272
|
+
}
|
|
273
|
+
/** The live session for this PR, whatever its id. Used by the routes that carry `?session=`
|
|
274
|
+
* only to tell "expired" apart from "superseded" — both refuse, with different sentences. */
|
|
275
|
+
export function peekSession(accountId, prId, now = Date.now()) {
|
|
276
|
+
sweep(now);
|
|
277
|
+
return sessions.get(keyOf(accountId, prId)) ?? null;
|
|
278
|
+
}
|
|
279
|
+
/** The session the caller named, or null — expired, superseded and never-existed are one
|
|
280
|
+
* answer here on purpose: all three mean "take your decisions again". */
|
|
281
|
+
export function getSession(accountId, prId, sessionId, now = Date.now()) {
|
|
282
|
+
sweep(now);
|
|
283
|
+
const rec = sessions.get(keyOf(accountId, prId));
|
|
284
|
+
if (!rec || rec.sessionId !== sessionId)
|
|
285
|
+
return null;
|
|
286
|
+
touch(rec, now);
|
|
287
|
+
return rec;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Drop a session by id. Returns false when it was already gone or already superseded — the
|
|
291
|
+
* DELETE route answers 204 either way, because "it is not there" is what the caller asked for.
|
|
292
|
+
*
|
|
293
|
+
* A session with a job in flight is NOT dropped: the reader closing the overlay must not leave
|
|
294
|
+
* a push writing into a record nobody holds. It expires on its own.
|
|
295
|
+
*/
|
|
296
|
+
export function dropSession(accountId, prId, sessionId, now = Date.now()) {
|
|
297
|
+
sweep(now);
|
|
298
|
+
const key = keyOf(accountId, prId);
|
|
299
|
+
const rec = sessions.get(key);
|
|
300
|
+
if (!rec || rec.sessionId !== sessionId)
|
|
301
|
+
return false;
|
|
302
|
+
if (rec.openRunning || rec.commitRunning)
|
|
303
|
+
return false;
|
|
304
|
+
endStream(rec);
|
|
305
|
+
sessions.delete(key);
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Claim the commit slot. SYNCHRONOUS for the same reason `claimSession` is: two clicks on
|
|
310
|
+
* "Commit" a tick apart must not become two pushes.
|
|
311
|
+
*/
|
|
312
|
+
export function claimCommitSlot(rec, now = Date.now()) {
|
|
313
|
+
if (rec.openRunning || rec.commitRunning)
|
|
314
|
+
return { ok: false, reason: 'pr' };
|
|
315
|
+
// `rec` holds no job (the line above proved it), so it contributes nothing to either count
|
|
316
|
+
// and needs no exclusion.
|
|
317
|
+
const refusal = capacityRefusal(rec.accountId);
|
|
318
|
+
if (refusal)
|
|
319
|
+
return { ok: false, reason: refusal };
|
|
320
|
+
const ac = new AbortController();
|
|
321
|
+
rec.commitRunning = true;
|
|
322
|
+
rec.commitAbort = ac;
|
|
323
|
+
rec.commit = { status: 'running', phase: 'preparing', result: null, error: null };
|
|
324
|
+
touch(rec, now);
|
|
325
|
+
return { ok: true, signal: ac.signal };
|
|
326
|
+
}
|
|
327
|
+
/* ═════════════════════════════ settling a session ═════════════════════════════ */
|
|
328
|
+
/** Prepare progress. Silently ignored once the session has settled, so a late phase callback
|
|
329
|
+
* from an abandoned build cannot walk a `ready` session backwards. */
|
|
330
|
+
export function setPreparePhase(rec, phase) {
|
|
331
|
+
if (rec.status !== 'preparing')
|
|
332
|
+
return;
|
|
333
|
+
rec.phase = phase;
|
|
334
|
+
emit(rec, { type: 'progress', session: sessionView(rec) });
|
|
335
|
+
}
|
|
336
|
+
export function settleReady(rec, model, modelHash, clean) {
|
|
337
|
+
rec.model = model;
|
|
338
|
+
rec.modelHash = modelHash;
|
|
339
|
+
rec.status = clean ? 'clean' : 'ready';
|
|
340
|
+
rec.phase = null;
|
|
341
|
+
rec.openRunning = false;
|
|
342
|
+
touch(rec, Date.now());
|
|
343
|
+
emit(rec, { type: 'ready', session: sessionView(rec) });
|
|
344
|
+
}
|
|
345
|
+
export function settleFailed(rec, code, message) {
|
|
346
|
+
rec.status = 'failed';
|
|
347
|
+
rec.phase = null;
|
|
348
|
+
rec.error = { code, message };
|
|
349
|
+
rec.openRunning = false;
|
|
350
|
+
touch(rec, Date.now());
|
|
351
|
+
emit(rec, { type: 'failed', session: sessionView(rec) });
|
|
352
|
+
}
|
|
353
|
+
export function setCommitPhase(rec, phase) {
|
|
354
|
+
if (rec.commit?.status !== 'running')
|
|
355
|
+
return;
|
|
356
|
+
rec.commit = { ...rec.commit, phase };
|
|
357
|
+
touch(rec, Date.now());
|
|
358
|
+
emit(rec, { type: 'commit_progress', session: sessionView(rec) });
|
|
359
|
+
}
|
|
360
|
+
export function settleCommitDone(rec, result) {
|
|
361
|
+
rec.commit = { status: 'done', phase: null, result, error: null };
|
|
362
|
+
rec.commitRunning = false;
|
|
363
|
+
rec.commitAbort = null;
|
|
364
|
+
touch(rec, Date.now());
|
|
365
|
+
emit(rec, { type: 'commit_done', session: sessionView(rec), result });
|
|
366
|
+
}
|
|
367
|
+
export function settleCommitFailed(rec, code, message) {
|
|
368
|
+
const error = { code, message };
|
|
369
|
+
rec.commit = { status: 'failed', phase: null, result: null, error };
|
|
370
|
+
rec.commitRunning = false;
|
|
371
|
+
rec.commitAbort = null;
|
|
372
|
+
touch(rec, Date.now());
|
|
373
|
+
emit(rec, { type: 'commit_failed', session: sessionView(rec), error });
|
|
374
|
+
}
|
|
375
|
+
/* ═════════════════════════════ the stream ═════════════════════════════ */
|
|
376
|
+
export function subscribe(rec, cb) {
|
|
377
|
+
rec.subscribers.add(cb);
|
|
378
|
+
return () => {
|
|
379
|
+
rec.subscribers.delete(cb);
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
/** ⚠ A subscriber that throws must not take the job down with it: the emitter is the build,
|
|
383
|
+
* and a broken socket is not a reason to abandon a push. */
|
|
384
|
+
function emit(rec, e) {
|
|
385
|
+
for (const cb of [...rec.subscribers]) {
|
|
386
|
+
try {
|
|
387
|
+
cb(e);
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
/* a dead socket — the stream's own close handler unsubscribes it */
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
/** Tell every live stream this session is gone, so the client stops waiting on a record that
|
|
395
|
+
* no longer exists. */
|
|
396
|
+
function endStream(rec) {
|
|
397
|
+
emit(rec, { type: 'done' });
|
|
398
|
+
rec.subscribers.clear();
|
|
399
|
+
}
|
|
400
|
+
/* ═════════════════════════════ Pro suggestions ═════════════════════════════ */
|
|
401
|
+
/**
|
|
402
|
+
* Store one validated suggestion and mint its handle.
|
|
403
|
+
*
|
|
404
|
+
* The HOST owns this, not the plugin: the plugin validates and hands back lines, and the id it
|
|
405
|
+
* would otherwise mint would be an id into a store the commit path cannot read. Valid for this
|
|
406
|
+
* session only — a new model is a new session, and a suggestion against the old bytes is
|
|
407
|
+
* `UnknownSuggestion` rather than a splice of text nobody looked at.
|
|
408
|
+
*/
|
|
409
|
+
export function storeSuggestion(rec, suggestion) {
|
|
410
|
+
const id = randomUUID();
|
|
411
|
+
rec.suggestions.set(id, suggestion);
|
|
412
|
+
touch(rec, Date.now());
|
|
413
|
+
return id;
|
|
414
|
+
}
|
|
415
|
+
/* ═════════════════════════════ manual edits (CORE) ═════════════════════════════ */
|
|
416
|
+
/**
|
|
417
|
+
* ⚠ A THIRD MEMORY BOUND, FOR THE ONE THING IN THIS FILE A READER CAN GROW BY TYPING.
|
|
418
|
+
*
|
|
419
|
+
* The record caps above bound how many MODELS the process retains; this bounds how much typed
|
|
420
|
+
* text one of them may accumulate — and since nothing is ever evicted (see `storeEdit`), it is
|
|
421
|
+
* the WHOLE bound. Without it the ceiling would be one edit per decidable region per save:
|
|
422
|
+
* `CONFLICT_MAX_FILES` × `CONFLICT_MAX_REGIONS` is 4,800 regions, at up to
|
|
423
|
+
* `conflictSuggestMaxChars` (4,000) each, so ~19 MiB per session before anybody re-saves — more
|
|
424
|
+
* than the 8 MiB the whole model is capped at, in a process every tenant shares. Sixty-four
|
|
425
|
+
* full-size edits is far more hand-editing than any resolution contains and well under a
|
|
426
|
+
* megabyte, so the refusal is reachable by a script and not by a person.
|
|
427
|
+
*
|
|
428
|
+
* ⚠ IT IS TIED TO THE PER-EDIT CAP RATHER THAN SPELLED AS A NUMBER, so the two move together.
|
|
429
|
+
*/
|
|
430
|
+
const MAX_EDIT_CHARS_PER_SESSION = 64 * config.conflictSuggestMaxChars;
|
|
431
|
+
const editChars = (edit) => {
|
|
432
|
+
let n = 0;
|
|
433
|
+
for (const line of edit.lines)
|
|
434
|
+
n += line.length + 1;
|
|
435
|
+
return n;
|
|
436
|
+
};
|
|
437
|
+
/**
|
|
438
|
+
* Store one validated edit and mint its handle, or null when this session is already holding
|
|
439
|
+
* as much typed text as it may.
|
|
440
|
+
*
|
|
441
|
+
* ⚠ A HANDLE IS NEVER EVICTED — `storeSuggestion`'s rule, and for the same reason. Re-saving a
|
|
442
|
+
* region used to DELETE its previous edit on the argument that "a region has one current text";
|
|
443
|
+
* true of the region, false of the handles pointing at it. The undo stack is the counter-example
|
|
444
|
+
* that shipped: it files `previousEditId` when a decision is replaced, so a second save followed
|
|
445
|
+
* by one Ctrl+Z restored an id the server had just destroyed. The pane could not tell — the
|
|
446
|
+
* client's own line map is append-only, so the region went on rendering applied-green and
|
|
447
|
+
* counting as decided — and the WHOLE commit then came back `UnknownEdit`, naming no file and no
|
|
448
|
+
* region, with every region on screen looking answered. A second tab saving the same region did
|
|
449
|
+
* it to the first tab's decisions from across the process. `MAX_EDIT_CHARS_PER_SESSION` is the
|
|
450
|
+
* bound now, and it is a real one.
|
|
451
|
+
*
|
|
452
|
+
* ⚠ AND THE BUDGET IS COMPUTED WITHOUT MUTATING, so a refusal is the no-op this route's contract
|
|
453
|
+
* promises ("a refusal mints nothing, so there is no handle to redeem"). The eviction ran BEFORE
|
|
454
|
+
* the sum, so a save that overran the cap destroyed the region's previous, accepted, already-
|
|
455
|
+
* decided edit on its way to answering `too_many_edits` — the same unlocatable `UnknownEdit` at
|
|
456
|
+
* commit time, reached by doing nothing wrong.
|
|
457
|
+
*
|
|
458
|
+
* ⚠ THE HOST MINTS THE ID, exactly as it does for a suggestion: an id minted anywhere else is
|
|
459
|
+
* an id into a store the commit path cannot read.
|
|
460
|
+
*/
|
|
461
|
+
export function storeEdit(rec, edit) {
|
|
462
|
+
let held = 0;
|
|
463
|
+
for (const e of rec.edits.values())
|
|
464
|
+
held += editChars(e);
|
|
465
|
+
if (held + editChars(edit) > MAX_EDIT_CHARS_PER_SESSION)
|
|
466
|
+
return null;
|
|
467
|
+
const id = randomUUID();
|
|
468
|
+
rec.edits.set(id, edit);
|
|
469
|
+
touch(rec, Date.now());
|
|
470
|
+
return id;
|
|
471
|
+
}
|
|
472
|
+
/* ═════════════════════════════ the wire projection ═════════════════════════════ */
|
|
473
|
+
/**
|
|
474
|
+
* The manifest. ⚠ NO REGIONS — a thirty-file conflict carrying them inline is a multi-megabyte
|
|
475
|
+
* payload on every progress frame; the SPA fetches one file's regions on selection (route 4).
|
|
476
|
+
*
|
|
477
|
+
* While `preparing` there is no model yet, so the pins are empty strings. The SPA reads them
|
|
478
|
+
* only once `status === 'ready'` (they are the key its decision store is filed under), and the
|
|
479
|
+
* `clean` sentence reads `baseRef`, which a clean build DOES carry a model for.
|
|
480
|
+
*/
|
|
481
|
+
export function sessionView(rec) {
|
|
482
|
+
const model = rec.model;
|
|
483
|
+
const files = model ? conflictFileEntries(model) : [];
|
|
484
|
+
return {
|
|
485
|
+
sessionId: rec.sessionId,
|
|
486
|
+
prId: rec.prId,
|
|
487
|
+
status: rec.status,
|
|
488
|
+
phase: rec.phase,
|
|
489
|
+
error: rec.error,
|
|
490
|
+
headSha: model?.headSha ?? '',
|
|
491
|
+
baseSha: model?.baseSha ?? '',
|
|
492
|
+
modelHash: rec.modelHash,
|
|
493
|
+
mergeBaseSha: model?.mergeBaseSha ?? null,
|
|
494
|
+
mergeBaseIsVirtual: model?.mergeBaseIsVirtual ?? false,
|
|
495
|
+
baseRef: model?.baseRef ?? '',
|
|
496
|
+
headRef: model?.headRef ?? '',
|
|
497
|
+
files,
|
|
498
|
+
unsupportedIndexes: files.filter((f) => f.unsupported !== null).map((f) => f.index),
|
|
499
|
+
// Every conflicted path merge-tree named is resolvable here: none refused, and the caps
|
|
500
|
+
// stopped us reaching none of them. `truncated` is the second half — a file we never
|
|
501
|
+
// extracted is a file the commit leaves conflicted just as surely as a binary one.
|
|
502
|
+
fullyResolvable: model !== null && !model.truncated && files.every((f) => f.unsupported === null),
|
|
503
|
+
autoApplied: rec.autoApply,
|
|
504
|
+
renameDetection: model?.renameDetection ?? 'on',
|
|
505
|
+
truncated: model?.truncated ?? false,
|
|
506
|
+
totalConflictedPaths: model?.totalConflictedPaths ?? 0,
|
|
507
|
+
strategies: model?.strategies ?? [],
|
|
508
|
+
rebaseUnavailableReason: model?.rebaseUnavailableReason ?? null,
|
|
509
|
+
// ⚠ `true` while there is no model, for the same reason the model itself defaults true: an
|
|
510
|
+
// absent fact is not a refusal, and the land route is what actually decides.
|
|
511
|
+
prBranchPushable: model?.prBranchPushable ?? true,
|
|
512
|
+
prBranchUnavailableReason: model?.prBranchUnavailableReason ?? null,
|
|
513
|
+
reservedBranchNames: model?.reservedBranchNames ?? [],
|
|
514
|
+
commit: rec.commit,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
/** Test seam only. */
|
|
518
|
+
export const __testing = {
|
|
519
|
+
sessions,
|
|
520
|
+
MAX_CONCURRENT_JOBS,
|
|
521
|
+
MAX_JOBS_PER_ACCOUNT,
|
|
522
|
+
MAX_SESSIONS_PER_ACCOUNT,
|
|
523
|
+
MAX_TOTAL_SESSIONS,
|
|
524
|
+
MAX_EDIT_CHARS_PER_SESSION,
|
|
525
|
+
reset: () => {
|
|
526
|
+
for (const rec of sessions.values())
|
|
527
|
+
rec.subscribers.clear();
|
|
528
|
+
sessions.clear();
|
|
529
|
+
shuttingDown = false;
|
|
530
|
+
},
|
|
531
|
+
};
|
|
532
|
+
//# sourceMappingURL=session.js.map
|