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,35 @@
|
|
|
1
|
+
// An explicit record rather than a ternary chain: a new BotWindowKind member is then a COMPILE
|
|
2
|
+
// ERROR here instead of silently falling through to 14 days. `rolling_90` was added for the
|
|
3
|
+
// Insights chat's 90d range and did exactly that under the old ternary.
|
|
4
|
+
const WINDOW_DAYS = {
|
|
5
|
+
rolling_7: 7,
|
|
6
|
+
rolling_14: 14,
|
|
7
|
+
rolling_30: 30,
|
|
8
|
+
rolling_90: 90,
|
|
9
|
+
sprint: 14,
|
|
10
|
+
};
|
|
11
|
+
export function botWindowDays(window) {
|
|
12
|
+
return WINDOW_DAYS[window];
|
|
13
|
+
}
|
|
14
|
+
export function botWindowMs(window) {
|
|
15
|
+
return botWindowDays(window) * 86_400_000;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 365.25 ÷ 12. The ONE factor that turns a rate measured over a window into a rate per month.
|
|
19
|
+
*
|
|
20
|
+
* ⚠ IT MUST EQUAL `DAYS_PER_MONTH` IN `packages/pro/src/bots/benchmark/cost.ts`, which the plugin
|
|
21
|
+
* declares separately because production plugin code may only `import type` from `shared` and may
|
|
22
|
+
* not import from core at all. Two numbers, one fact: if these ever diverge, the Bots → ROI table
|
|
23
|
+
* and the Bots → Benchmark rollup will quote different money for the same bot at the same price,
|
|
24
|
+
* one tab apart. `benchmark-cost.test.ts` pins the plugin's to six places; `bot-analytics` tests
|
|
25
|
+
* pin this one.
|
|
26
|
+
*
|
|
27
|
+
* ⚠ AND IT IS LOAD-BEARING, NOT COSMETIC. `costPerActedOnUsd` shipped as `monthlyPrice ÷ actedOn`,
|
|
28
|
+
* where `actedOn` is counted over the SELECTED WINDOW — a month's numerator over a fortnight's
|
|
29
|
+
* denominator. On a real reviewer that read $2.23 beside the benchmark's $21.24 for the same bot
|
|
30
|
+
* and the same $49, with no way for a reader to reconcile them, and it erred in the direction that
|
|
31
|
+
* flatters the bot. Both figures are now monthly rates; they differ only by the window each was
|
|
32
|
+
* measured over, which each surface states.
|
|
33
|
+
*/
|
|
34
|
+
export const DAYS_PER_MONTH = 30.44;
|
|
35
|
+
//# sourceMappingURL=bot-window.js.map
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { and, desc, eq, gte, inArray } from 'drizzle-orm';
|
|
2
|
+
import { db, schema } from './client.js';
|
|
3
|
+
const { repos, users, branchCommits, pullRequests } = schema;
|
|
4
|
+
// How many merged PRs the expanded row lists per repo. DELIBERATELY far under what the sync
|
|
5
|
+
// retains (`BRANCH_COMMIT_WINDOW` = 100 commits, feeding `getBranchTrends`): the list is a
|
|
6
|
+
// glance, deeper history is the trend strip's job — and the cap keeps the hot workspace-wide
|
|
7
|
+
// strip's WIRE payload lean. ⚠ The DB read below is bounded by BRANCH_COMMIT_WINDOW × repos,
|
|
8
|
+
// not this cap — the JS loop discards the surplus. Accepted for now (indexed read, 60s client
|
|
9
|
+
// cache); if it ever matters, the fix is a per-repo windowed subquery, not a lower sync window.
|
|
10
|
+
const READ_PR_CAP = 10;
|
|
11
|
+
/** The key `trunkHeadFailingChecks` answers under: a sha is unique only WITHIN a repo. */
|
|
12
|
+
export function trunkHeadKey(repoId, sha) {
|
|
13
|
+
return `${repoId}:${sha}`;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* THE ONE READER OF A DEFAULT-BRANCH HEAD'S FAILING CHECKS — `/api/branch-status` and the Pending
|
|
17
|
+
* board's red-trunk cards (ci_failing's trunk arm, the promoted `trunk_red` my_turn card) both read
|
|
18
|
+
* through it, so the two surfaces cannot name different checks for one red head.
|
|
19
|
+
*
|
|
20
|
+
* The head's own `branch_commits` row holds them (written by the branch snapshot in the same
|
|
21
|
+
* transaction as `repos.defaultBranchCiStatus`). Matched by SHA, NEVER by position: the commit
|
|
22
|
+
* window is ordered by committer date, and a backdated committer date (a rebase, or a cherry-pick
|
|
23
|
+
* with --committer-date-is-author-date) can sort the head anywhere in it.
|
|
24
|
+
*
|
|
25
|
+
* Account-scoped, one query over the `(accountId, repoId, sha)` unique. The `inArray × inArray`
|
|
26
|
+
* predicate over-matches (repo A's row for a sha only repo B asked about), so the result is keyed
|
|
27
|
+
* by the PAIR (`trunkHeadKey`) and only requested pairs are kept. A head with no row — outside the
|
|
28
|
+
* stored window, or not synced yet — or a green one (NULL) is simply absent: "no names", never
|
|
29
|
+
* "nothing failed".
|
|
30
|
+
*/
|
|
31
|
+
export async function trunkHeadFailingChecks(accountId, heads) {
|
|
32
|
+
const out = new Map();
|
|
33
|
+
if (heads.length === 0)
|
|
34
|
+
return out;
|
|
35
|
+
const wanted = new Set(heads.map((h) => trunkHeadKey(h.repoId, h.sha)));
|
|
36
|
+
const rows = await db
|
|
37
|
+
.select({
|
|
38
|
+
repoId: branchCommits.repoId,
|
|
39
|
+
sha: branchCommits.sha,
|
|
40
|
+
failingChecks: branchCommits.failingChecks,
|
|
41
|
+
})
|
|
42
|
+
.from(branchCommits)
|
|
43
|
+
.where(and(eq(branchCommits.accountId, accountId), inArray(branchCommits.repoId, [...new Set(heads.map((h) => h.repoId))]), inArray(branchCommits.sha, [...new Set(heads.map((h) => h.sha))])))
|
|
44
|
+
.execute();
|
|
45
|
+
for (const r of rows) {
|
|
46
|
+
const key = trunkHeadKey(r.repoId, r.sha);
|
|
47
|
+
if (r.failingChecks != null && wanted.has(key))
|
|
48
|
+
out.set(key, r.failingChecks);
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Default-branch health for every repo in scope: the stored head snapshot plus that branch's
|
|
54
|
+
* recent commits.
|
|
55
|
+
*
|
|
56
|
+
* Account-scoped on BOTH tables independently. `branch_commits.accountId` is denormalized
|
|
57
|
+
* precisely so the commit read is a single indexed predicate rather than a join back through
|
|
58
|
+
* `repos` — but the repo listing is scoped too, so a `repoIds` array naming another tenant's
|
|
59
|
+
* repos selects nothing rather than leaking rows.
|
|
60
|
+
*
|
|
61
|
+
* Repos with no synced branch data still appear, with nulls: the client renders one row per
|
|
62
|
+
* repo in the visible set, and "unknown" is the truthful state for a repo added a minute ago.
|
|
63
|
+
*
|
|
64
|
+
* `repoIds` null/empty ⇒ every repo the account owns.
|
|
65
|
+
*/
|
|
66
|
+
export async function getBranchStatus(accountId, repoIds) {
|
|
67
|
+
const scoped = repoIds != null && repoIds.length > 0;
|
|
68
|
+
const repoRows = await db
|
|
69
|
+
.select({
|
|
70
|
+
id: repos.id,
|
|
71
|
+
branchName: repos.defaultBranchName,
|
|
72
|
+
headSha: repos.defaultBranchHeadSha,
|
|
73
|
+
ciStatus: repos.defaultBranchCiStatus,
|
|
74
|
+
updatedAt: repos.defaultBranchUpdatedAt,
|
|
75
|
+
})
|
|
76
|
+
.from(repos)
|
|
77
|
+
.where(scoped
|
|
78
|
+
? and(eq(repos.accountId, accountId), inArray(repos.id, repoIds))
|
|
79
|
+
: eq(repos.accountId, accountId))
|
|
80
|
+
.execute();
|
|
81
|
+
if (repoRows.length === 0)
|
|
82
|
+
return { repos: [] };
|
|
83
|
+
const ids = repoRows.map((r) => r.id);
|
|
84
|
+
// One pass over the account's commits for the scoped repos, newest first; grouped into their
|
|
85
|
+
// merged PRs in JS. The whole set is at most `BRANCH_COMMIT_WINDOW × repos` rows (the sync
|
|
86
|
+
// trims per repo; the grouping below then keeps only READ_PR_CAP PR groups of each), so there
|
|
87
|
+
// is no per-repo query fan-out and no unbounded result. Commit authors are deliberately not
|
|
88
|
+
// selected any more — the listed unit is the PR, whose author comes off the PR row below.
|
|
89
|
+
const commitRows = await db
|
|
90
|
+
.select({
|
|
91
|
+
repoId: branchCommits.repoId,
|
|
92
|
+
sha: branchCommits.sha,
|
|
93
|
+
messageHeadline: branchCommits.messageHeadline,
|
|
94
|
+
committedAt: branchCommits.committedAt,
|
|
95
|
+
ciStatus: branchCommits.ciStatus,
|
|
96
|
+
prNumber: branchCommits.prNumber,
|
|
97
|
+
})
|
|
98
|
+
.from(branchCommits)
|
|
99
|
+
.where(and(eq(branchCommits.accountId, accountId), inArray(branchCommits.repoId, ids)))
|
|
100
|
+
.orderBy(desc(branchCommits.committedAt))
|
|
101
|
+
.execute();
|
|
102
|
+
// Resolve each commit's associated PR NUMBER (stored by the branch sync) to the LOCAL
|
|
103
|
+
// pull_requests row, so the client's chip can open that PR's own detail tab instead of leaving
|
|
104
|
+
// the app. One extra query, bounded by the distinct numbers across the scoped repos' windows.
|
|
105
|
+
//
|
|
106
|
+
// Isolation: the predicate carries `accountId` AND restricts to `ids` — the repo ids already
|
|
107
|
+
// resolved from the account-scoped `repos` listing above — so a number can only ever match a PR
|
|
108
|
+
// THIS account synced. A number that belongs to another tenant's data cannot resolve.
|
|
109
|
+
//
|
|
110
|
+
// The map key is `(repoId, number)`, NEVER the bare number: PR numbers are unique only WITHIN a
|
|
111
|
+
// repo, so a number-keyed map would cross-link repo A's #12 onto repo B's commit and open the
|
|
112
|
+
// wrong PR. The `inArray × inArray` predicate deliberately over-matches (it can return repo A's
|
|
113
|
+
// #12 when only repo B asked for it); keying by the pair is what makes that harmless. A future
|
|
114
|
+
// "simplification" to a bare number key is silently wrong, which is why there is a seeded test
|
|
115
|
+
// for it rather than only this comment.
|
|
116
|
+
const prNumbers = [
|
|
117
|
+
...new Set(commitRows.map((c) => c.prNumber).filter((n) => n != null)),
|
|
118
|
+
];
|
|
119
|
+
const prByRepoNumber = new Map();
|
|
120
|
+
if (prNumbers.length > 0) {
|
|
121
|
+
const prRows = await db
|
|
122
|
+
.select({
|
|
123
|
+
id: pullRequests.id,
|
|
124
|
+
repoId: pullRequests.repoId,
|
|
125
|
+
number: pullRequests.number,
|
|
126
|
+
title: pullRequests.title,
|
|
127
|
+
mergedAt: pullRequests.mergedAt,
|
|
128
|
+
authorLogin: users.githubLogin,
|
|
129
|
+
authorAvatarUrl: users.avatarUrl,
|
|
130
|
+
})
|
|
131
|
+
.from(pullRequests)
|
|
132
|
+
.leftJoin(users, eq(users.id, pullRequests.authorId))
|
|
133
|
+
.where(and(eq(pullRequests.accountId, accountId), inArray(pullRequests.repoId, ids), inArray(pullRequests.number, prNumbers)))
|
|
134
|
+
.execute();
|
|
135
|
+
for (const p of prRows)
|
|
136
|
+
prByRepoNumber.set(`${p.repoId}:${p.number}`, {
|
|
137
|
+
id: p.id,
|
|
138
|
+
title: p.title,
|
|
139
|
+
mergedAt: p.mergedAt,
|
|
140
|
+
authorLogin: p.authorLogin ?? null,
|
|
141
|
+
authorAvatarUrl: p.authorAvatarUrl ?? null,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
// The HEAD commit's failing checks, through THE ONE READER the Pending cards use too — matched by
|
|
145
|
+
// SHA, never by position (see `trunkHeadFailingChecks`).
|
|
146
|
+
const failingByRepoSha = await trunkHeadFailingChecks(accountId, repoRows.flatMap((r) => (r.headSha != null ? [{ repoId: r.id, sha: r.headSha }] : [])));
|
|
147
|
+
const groupsByRepo = new Map();
|
|
148
|
+
const lastCommitAtByRepo = new Map();
|
|
149
|
+
for (const c of commitRows) {
|
|
150
|
+
// Newest raw commit per repo — INDEPENDENT of PR membership, so a direct-push-only repo
|
|
151
|
+
// still reads as recently active (the newest commit's own timestamp, never our observation
|
|
152
|
+
// time: `defaultBranchUpdatedAt` is when we LOOKED, which would make an idle repo read as
|
|
153
|
+
// freshly active).
|
|
154
|
+
if (!lastCommitAtByRepo.has(c.repoId))
|
|
155
|
+
lastCommitAtByRepo.set(c.repoId, c.committedAt);
|
|
156
|
+
if (c.prNumber == null)
|
|
157
|
+
continue;
|
|
158
|
+
const groups = groupsByRepo.get(c.repoId) ?? new Map();
|
|
159
|
+
groupsByRepo.set(c.repoId, groups);
|
|
160
|
+
const g = groups.get(c.prNumber);
|
|
161
|
+
if (g == null) {
|
|
162
|
+
groups.set(c.prNumber, {
|
|
163
|
+
prNumber: c.prNumber,
|
|
164
|
+
newestCommitAt: c.committedAt,
|
|
165
|
+
// The group's rollup is its NEWEST commit's — for a merge-commit PR that is the merge
|
|
166
|
+
// commit itself, i.e. the trunk state this PR produced. Nullable column ("no CI
|
|
167
|
+
// observed") reads as 'unknown' on the wire.
|
|
168
|
+
ciStatus: (c.ciStatus ?? 'unknown'),
|
|
169
|
+
commits: [{ sha: c.sha, messageHeadline: c.messageHeadline }],
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
g.commits.push({ sha: c.sha, messageHeadline: c.messageHeadline });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
repos: repoRows.map((r) => {
|
|
178
|
+
const groups = [...(groupsByRepo.get(r.id)?.values() ?? [])];
|
|
179
|
+
const mergedPrs = groups
|
|
180
|
+
.map((g) => {
|
|
181
|
+
const pr = prByRepoNumber.get(`${r.id}:${g.prNumber}`);
|
|
182
|
+
return {
|
|
183
|
+
prNumber: g.prNumber,
|
|
184
|
+
// Null while prNumber is set means "that PR isn't synced for this account"
|
|
185
|
+
// (squash-merged before the backfill window, or a repo added later): the client
|
|
186
|
+
// links out to github.com rather than dropping the reference.
|
|
187
|
+
prId: pr?.id ?? null,
|
|
188
|
+
title: pr?.title ?? null,
|
|
189
|
+
authorLogin: pr?.authorLogin ?? null,
|
|
190
|
+
authorAvatarUrl: pr?.authorAvatarUrl ?? null,
|
|
191
|
+
// "In the order they were merged": the PR row's mergedAt when resolved, else the
|
|
192
|
+
// newest consolidated commit's time — never null, so the sort below is total.
|
|
193
|
+
mergedAt: (pr?.mergedAt ?? g.newestCommitAt).toISOString(),
|
|
194
|
+
ciStatus: g.ciStatus,
|
|
195
|
+
commits: g.commits,
|
|
196
|
+
};
|
|
197
|
+
})
|
|
198
|
+
.sort((a, b) => b.mergedAt.localeCompare(a.mergedAt))
|
|
199
|
+
.slice(0, READ_PR_CAP);
|
|
200
|
+
return {
|
|
201
|
+
repoId: r.id,
|
|
202
|
+
branchName: r.branchName,
|
|
203
|
+
headSha: r.headSha,
|
|
204
|
+
ciStatus: (r.ciStatus ?? 'unknown'),
|
|
205
|
+
// Derived, not a second stored copy: the head commit's own row already holds its failures.
|
|
206
|
+
// Matched by SHA (see trunkHeadFailingChecks); no match — a trimmed head, or nothing
|
|
207
|
+
// synced yet — degrades to the CI label alone.
|
|
208
|
+
failingChecks: r.headSha != null ? (failingByRepoSha.get(trunkHeadKey(r.id, r.headSha)) ?? []) : [],
|
|
209
|
+
lastCommitAt: lastCommitAtByRepo.get(r.id)?.toISOString() ?? null,
|
|
210
|
+
mergedPrs,
|
|
211
|
+
};
|
|
212
|
+
}),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
// Trend window for `getBranchTrends`. Exported because the sync side keys off the SAME number
|
|
216
|
+
// twice: the trim in sync/branch-status.ts retains rows inside this window even beyond its
|
|
217
|
+
// 100-commit bound (so the one-time history backfill survives the next tick), and the backfill
|
|
218
|
+
// itself fetches `history(since: now − TREND_DAYS)`. This read filter is ALSO where the "no
|
|
219
|
+
// year-old bars" promise lives — the writer never deletes a row the newest-100 bound protects,
|
|
220
|
+
// however old (see the trim's comment).
|
|
221
|
+
export const TREND_DAYS = 90;
|
|
222
|
+
const DAY_MS = 86_400_000;
|
|
223
|
+
/**
|
|
224
|
+
* The ONE lazy series behind an expanded default-branch row: per UTC day, failing trunk commits
|
|
225
|
+
* (the DayStrip cells) and PRs merged into the default branch (the line band above them) — a
|
|
226
|
+
* single shared axis, dense from the oldest retained commit day to today, so the two facts
|
|
227
|
+
* align cell-for-cell.
|
|
228
|
+
*
|
|
229
|
+
* Fetched only when a row expands — deliberately NOT part of `getBranchStatus`, whose payload is
|
|
230
|
+
* a hot workspace-wide read. Bucketed in JS rather than SQL date functions (dialect-divergent),
|
|
231
|
+
* which is fine because the sync trims `branch_commits` to ≤100 rows per repo and the merged-PR
|
|
232
|
+
* read is bounded by the axis window.
|
|
233
|
+
*
|
|
234
|
+
* Returns null when the repo isn't owned by the account (→ 404 at the route).
|
|
235
|
+
*/
|
|
236
|
+
export async function getBranchTrends(accountId, repoId) {
|
|
237
|
+
const repoRows = await db
|
|
238
|
+
.select({ id: repos.id, defaultBranchName: repos.defaultBranchName })
|
|
239
|
+
.from(repos)
|
|
240
|
+
.where(and(eq(repos.id, repoId), eq(repos.accountId, accountId)))
|
|
241
|
+
.execute();
|
|
242
|
+
const repo = repoRows[0];
|
|
243
|
+
if (repo == null)
|
|
244
|
+
return null;
|
|
245
|
+
const now = Date.now();
|
|
246
|
+
const commitRows = await db
|
|
247
|
+
.select({ committedAt: branchCommits.committedAt, ciStatus: branchCommits.ciStatus })
|
|
248
|
+
.from(branchCommits)
|
|
249
|
+
.where(and(eq(branchCommits.accountId, accountId), eq(branchCommits.repoId, repoId), gte(branchCommits.committedAt, new Date(now - TREND_DAYS * DAY_MS))))
|
|
250
|
+
.execute();
|
|
251
|
+
const failedByDay = new Map();
|
|
252
|
+
const passedByDay = new Map();
|
|
253
|
+
for (const c of commitRows) {
|
|
254
|
+
const day = c.committedAt.toISOString().slice(0, 10);
|
|
255
|
+
// "Failed" is the red rollup pair — the same predicate as the strip's own failing count —
|
|
256
|
+
// and "passed" is 'success' alone: pending/unknown/null are neither, so a day's cell can
|
|
257
|
+
// legitimately show fewer commits than landed.
|
|
258
|
+
if (c.ciStatus === 'failure' || c.ciStatus === 'error') {
|
|
259
|
+
failedByDay.set(day, (failedByDay.get(day) ?? 0) + 1);
|
|
260
|
+
}
|
|
261
|
+
else if (c.ciStatus === 'success') {
|
|
262
|
+
passedByDay.set(day, (passedByDay.get(day) ?? 0) + 1);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// The axis is dense from the OLDEST RETAINED commit day to today, zero-filled — but never
|
|
266
|
+
// padded back to the full 90 days: on a busy repo the 100-commit cap trims inside the date
|
|
267
|
+
// window, and days before the oldest retained commit are trimmed history, not quiet days.
|
|
268
|
+
// Padding them would draw a green past we never observed. The merged-PR line shares this
|
|
269
|
+
// axis on purpose (that is the whole point of the single chart), so it is truncated to the
|
|
270
|
+
// same span even though pullRequests could answer further back.
|
|
271
|
+
const oldestDay = commitRows
|
|
272
|
+
.map((c) => c.committedAt.toISOString().slice(0, 10))
|
|
273
|
+
.sort()[0];
|
|
274
|
+
const daily = [];
|
|
275
|
+
if (oldestDay != null) {
|
|
276
|
+
const startMs = Date.parse(`${oldestDay}T00:00:00Z`);
|
|
277
|
+
const endMs = Date.parse(`${new Date(now).toISOString().slice(0, 10)}T00:00:00Z`);
|
|
278
|
+
// PRs merged into the default branch, counted per UTC day of `mergedAt`. `baseRefName =
|
|
279
|
+
// <default branch>` excludes NULL bases by construction (SQL equality) and on PURPOSE — an
|
|
280
|
+
// unhydrated base is not evidence the PR landed on trunk. A repo whose default branch name
|
|
281
|
+
// we haven't synced yet has nothing attributable, so its line is flat zero rather than a
|
|
282
|
+
// guess.
|
|
283
|
+
const mergedByDay = new Map();
|
|
284
|
+
if (repo.defaultBranchName != null) {
|
|
285
|
+
const mergedRows = await db
|
|
286
|
+
.select({ mergedAt: pullRequests.mergedAt })
|
|
287
|
+
.from(pullRequests)
|
|
288
|
+
.where(and(eq(pullRequests.accountId, accountId), eq(pullRequests.repoId, repoId), eq(pullRequests.baseRefName, repo.defaultBranchName), gte(pullRequests.mergedAt, new Date(startMs))))
|
|
289
|
+
.execute();
|
|
290
|
+
for (const p of mergedRows) {
|
|
291
|
+
// The predicate already excludes NULL mergedAt; the guard is for the type, not the data.
|
|
292
|
+
if (p.mergedAt == null)
|
|
293
|
+
continue;
|
|
294
|
+
const day = p.mergedAt.toISOString().slice(0, 10);
|
|
295
|
+
mergedByDay.set(day, (mergedByDay.get(day) ?? 0) + 1);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
for (let ms = startMs; ms <= endMs; ms += DAY_MS) {
|
|
299
|
+
const day = new Date(ms).toISOString().slice(0, 10);
|
|
300
|
+
daily.push({
|
|
301
|
+
day,
|
|
302
|
+
failed: failedByDay.get(day) ?? 0,
|
|
303
|
+
passed: passedByDay.get(day) ?? 0,
|
|
304
|
+
merged: mergedByDay.get(day) ?? 0,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return { repoId, daily };
|
|
309
|
+
}
|
|
310
|
+
//# sourceMappingURL=branch-queries.js.map
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { isNonCodeFile } from './code-loc.js';
|
|
2
|
+
// ============================================================================
|
|
3
|
+
// CHANGE SHAPE — is this pull request's code churn actually CODE?
|
|
4
|
+
// ============================================================================
|
|
5
|
+
//
|
|
6
|
+
// Blast radius asks how far a change can REACH, and answers it from the file paths. That is the
|
|
7
|
+
// right question and the wrong half of the evidence in one specific case: a change to the
|
|
8
|
+
// COMMENTS inside a high-consequence file. A real one —
|
|
9
|
+
//
|
|
10
|
+
// golang/go#80721 "crypto/hpke: document sequence counter size"
|
|
11
|
+
// src/crypto/hpke/hpke.go, +4 −2, every changed line a `//` doc comment
|
|
12
|
+
//
|
|
13
|
+
// — landed as HIGH, because `crypto/` is one of the contract surfaces. The path is right about
|
|
14
|
+
// the file and wrong about the change. This module reads the PATCH and says which.
|
|
15
|
+
//
|
|
16
|
+
// ---- WHAT IT ANSWERS, AND WHAT IT DELIBERATELY DOES NOT --------------------
|
|
17
|
+
//
|
|
18
|
+
// 'comments' — every changed line in every code file is a comment or blank.
|
|
19
|
+
// 'formatting' — the added and removed lines are the SAME lines modulo whitespace.
|
|
20
|
+
// 'code' — something else changed. The overwhelming majority.
|
|
21
|
+
// null — we could not tell, and that is a first-class answer (see below).
|
|
22
|
+
//
|
|
23
|
+
// ⚠ IMPORTS WERE CONSIDERED AND DELIBERATELY LEFT OUT. Reordering imports is trivial; ADDING one
|
|
24
|
+
// means the file now uses something it did not, which is a real change with real reach. The two
|
|
25
|
+
// are one line apart in a diff and telling them apart needs more than a line matcher, so the
|
|
26
|
+
// honest move is to not claim either. If it is ever added it belongs beside `deps` — carried for
|
|
27
|
+
// narration, never as a reason to lower a level.
|
|
28
|
+
//
|
|
29
|
+
// ---- THE CONSERVATIVE RULE, WHICH IS THE WHOLE DESIGN ----------------------
|
|
30
|
+
//
|
|
31
|
+
// ⚠ EVERY UNCERTAINTY RESOLVES TO "NOT TRIVIAL". An unknown file extension, a patch GitHub did
|
|
32
|
+
// not give us, a block comment we cannot bound — all of them return `code` or `null`, never
|
|
33
|
+
// `comments`. That asymmetry is deliberate and it is the only thing that makes this safe to wire
|
|
34
|
+
// into a level: the failure mode of a wrong `code` is a pull request that stays HIGH and gets
|
|
35
|
+
// read carefully, and the failure mode of a wrong `comments` is a maintainer waving through a
|
|
36
|
+
// change to a crypto file. Those are not equally bad.
|
|
37
|
+
/** Single-line comment markers by file extension. An extension absent from this map is a
|
|
38
|
+
* language we do not claim to understand, and its file makes the whole answer `code`. */
|
|
39
|
+
const LINE_COMMENT = {
|
|
40
|
+
// C-family and friends
|
|
41
|
+
ts: ['//'], tsx: ['//'], js: ['//'], jsx: ['//'], mjs: ['//'], cjs: ['//'], mts: ['//'], cts: ['//'],
|
|
42
|
+
go: ['//'], java: ['//'], c: ['//'], h: ['//'], cc: ['//'], cpp: ['//'], hpp: ['//'], cxx: ['//'],
|
|
43
|
+
cs: ['//'], rs: ['//'], swift: ['//'], kt: ['//'], kts: ['//'], scala: ['//'], dart: ['//'],
|
|
44
|
+
php: ['//', '#'], m: ['//'], mm: ['//'], zig: ['//'], v: ['//'], groovy: ['//'],
|
|
45
|
+
// Hash-comment languages
|
|
46
|
+
py: ['#'], rb: ['#'], sh: ['#'], bash: ['#'], zsh: ['#'], pl: ['#'], pm: ['#'], r: ['#'],
|
|
47
|
+
ex: ['#'], exs: ['#'], nim: ['#'], cr: ['#'], tcl: ['#'],
|
|
48
|
+
// Dash-comment languages
|
|
49
|
+
sql: ['--'], lua: ['--'], hs: ['--'], elm: ['--'], ada: ['--'],
|
|
50
|
+
// Others
|
|
51
|
+
erl: ['%'], hrl: ['%'], tex: ['%'], clj: [';'], cljs: [';'], el: [';'], lisp: [';'], scm: [';'],
|
|
52
|
+
vim: ['"'], ini: [';', '#'], jl: ['#'],
|
|
53
|
+
};
|
|
54
|
+
/** Block comment delimiters by extension. Absent = the language has none we track, and a file
|
|
55
|
+
* whose changed lines are not all single-line comments is simply `code`. */
|
|
56
|
+
const BLOCK_COMMENT = {
|
|
57
|
+
ts: [['/*', '*/']], tsx: [['/*', '*/']], js: [['/*', '*/']], jsx: [['/*', '*/']],
|
|
58
|
+
mjs: [['/*', '*/']], cjs: [['/*', '*/']], mts: [['/*', '*/']], cts: [['/*', '*/']],
|
|
59
|
+
go: [['/*', '*/']], java: [['/*', '*/']], c: [['/*', '*/']], h: [['/*', '*/']],
|
|
60
|
+
cc: [['/*', '*/']], cpp: [['/*', '*/']], hpp: [['/*', '*/']], cxx: [['/*', '*/']],
|
|
61
|
+
cs: [['/*', '*/']], rs: [['/*', '*/']], swift: [['/*', '*/']], kt: [['/*', '*/']],
|
|
62
|
+
kts: [['/*', '*/']], scala: [['/*', '*/']], dart: [['/*', '*/']], php: [['/*', '*/']],
|
|
63
|
+
m: [['/*', '*/']], mm: [['/*', '*/']], groovy: [['/*', '*/']], sql: [['/*', '*/']],
|
|
64
|
+
// ⚠ Python's triple-quoted strings are STRINGS, not comments — but a changed docstring is the
|
|
65
|
+
// exact case this module exists for, and a bare triple-quoted expression statement has no other
|
|
66
|
+
// use. Tracked as a block, and the conservative rule covers the misreads: a multi-line string
|
|
67
|
+
// assigned to a variable would be misclassified as a comment, so a changed one reports
|
|
68
|
+
// `comments` when it is really data. That is the one known false-trivial, it is bounded to
|
|
69
|
+
// Python, and it still cannot take a level below MEDIUM.
|
|
70
|
+
py: [['"""', '"""'], ["'''", "'''"]],
|
|
71
|
+
lua: [['--[[', ']]']],
|
|
72
|
+
};
|
|
73
|
+
function extensionOf(path) {
|
|
74
|
+
const p = path.replace(/\\/g, '/').toLowerCase();
|
|
75
|
+
const base = p.slice(p.lastIndexOf('/') + 1);
|
|
76
|
+
const i = base.lastIndexOf('.');
|
|
77
|
+
if (i <= 0)
|
|
78
|
+
return '';
|
|
79
|
+
return base.slice(i + 1);
|
|
80
|
+
}
|
|
81
|
+
/** Split a REST `patch` into hunks, each a list of marked lines. Header lines (`@@`, `\ No
|
|
82
|
+
* newline at end of file`) are dropped. */
|
|
83
|
+
function hunksOf(patch) {
|
|
84
|
+
const out = [];
|
|
85
|
+
let cur = null;
|
|
86
|
+
for (const raw of patch.split('\n')) {
|
|
87
|
+
if (raw.startsWith('@@')) {
|
|
88
|
+
cur = [];
|
|
89
|
+
out.push(cur);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (cur == null)
|
|
93
|
+
continue;
|
|
94
|
+
if (raw.startsWith('\\'))
|
|
95
|
+
continue; // ""
|
|
96
|
+
const marker = raw[0];
|
|
97
|
+
if (marker === '+' || marker === '-' || marker === ' ') {
|
|
98
|
+
cur.push({ marker, text: raw.slice(1) });
|
|
99
|
+
}
|
|
100
|
+
else if (raw === '') {
|
|
101
|
+
// An empty line in a patch is a context line whose content is empty.
|
|
102
|
+
cur.push({ marker: ' ', text: '' });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Are every one of this hunk's CHANGED lines a comment or blank?
|
|
109
|
+
*
|
|
110
|
+
* Block state is tracked across the whole hunk INCLUDING context lines, because those are what
|
|
111
|
+
* tell us whether we are inside a comment when a changed line arrives.
|
|
112
|
+
*
|
|
113
|
+
* ⚠ A HUNK CAN BEGIN INSIDE A BLOCK COMMENT and the patch does not say so. That is detectable:
|
|
114
|
+
* if the first delimiter we meet is a CLOSE, we started inside one. Anything we still cannot
|
|
115
|
+
* bound returns false.
|
|
116
|
+
*/
|
|
117
|
+
function hunkIsCommentOnly(lines, singles, blocks) {
|
|
118
|
+
// Pre-scan for the "started inside a block" case.
|
|
119
|
+
let inBlock = null;
|
|
120
|
+
if (blocks.length > 0) {
|
|
121
|
+
outer: for (const l of lines) {
|
|
122
|
+
const t = l.text;
|
|
123
|
+
for (const b of blocks) {
|
|
124
|
+
const openAt = t.indexOf(b[0]);
|
|
125
|
+
const closeAt = t.indexOf(b[1]);
|
|
126
|
+
if (openAt !== -1 && (closeAt === -1 || closeAt > openAt))
|
|
127
|
+
break outer; // opened first
|
|
128
|
+
if (closeAt !== -1) {
|
|
129
|
+
inBlock = b; // a close with no open before it — the hunk began inside this block
|
|
130
|
+
break outer;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
for (const l of lines) {
|
|
136
|
+
const trimmed = l.text.trim();
|
|
137
|
+
const changed = l.marker !== ' ';
|
|
138
|
+
let isCommentLine = inBlock != null;
|
|
139
|
+
if (inBlock != null) {
|
|
140
|
+
// Inside a block: it stays a comment line, and may close here.
|
|
141
|
+
if (trimmed.includes(inBlock[1])) {
|
|
142
|
+
const after = trimmed.slice(trimmed.indexOf(inBlock[1]) + inBlock[1].length).trim();
|
|
143
|
+
inBlock = null;
|
|
144
|
+
// Code after the closing delimiter on the same line means this is not purely a comment.
|
|
145
|
+
if (after !== '' && changed)
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else if (trimmed === '') {
|
|
150
|
+
isCommentLine = true;
|
|
151
|
+
}
|
|
152
|
+
else if (singles.some((s) => trimmed.startsWith(s))) {
|
|
153
|
+
isCommentLine = true;
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
// Does a block open here, with nothing but whitespace before it?
|
|
157
|
+
const opener = blocks.find((b) => trimmed.startsWith(b[0]));
|
|
158
|
+
if (opener) {
|
|
159
|
+
isCommentLine = true;
|
|
160
|
+
const rest = trimmed.slice(opener[0].length);
|
|
161
|
+
const closeAt = rest.indexOf(opener[1]);
|
|
162
|
+
if (closeAt === -1) {
|
|
163
|
+
inBlock = opener;
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
// Opened and closed on one line; anything after it is code.
|
|
167
|
+
const after = rest.slice(closeAt + opener[1].length).trim();
|
|
168
|
+
if (after !== '' && changed)
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (changed && !isCommentLine)
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
// ⚠ An unterminated block at the end of the hunk means we never saw it close. The lines we
|
|
177
|
+
// accepted were inside it, which is fine — the state simply does not carry to the next hunk,
|
|
178
|
+
// and each hunk re-derives its own.
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
/** Whitespace-insensitive key for the formatting comparison. */
|
|
182
|
+
const wsKey = (s) => s.replace(/\s+/g, '');
|
|
183
|
+
/**
|
|
184
|
+
* Classify one pull request's code churn from its per-file patches.
|
|
185
|
+
*
|
|
186
|
+
* `files` is the REST `/pulls/:n/files` shape. A file with no `patch` (GitHub omits it for
|
|
187
|
+
* binaries and for very large diffs) makes the answer `null` — we did not see the change, so we
|
|
188
|
+
* do not get to describe it.
|
|
189
|
+
*
|
|
190
|
+
* ⚠ NON-CODE FILES ARE EXCLUDED, not judged. Documentation and config have their own place in
|
|
191
|
+
* the blast fold (`nonCodeFiles`); a README beside a comment-only Go change must not make the
|
|
192
|
+
* answer `code`. If NO code file remains there is nothing for this module to say — the existing
|
|
193
|
+
* `codeFiles === 0` rules already handle that pull request — so it returns null.
|
|
194
|
+
*/
|
|
195
|
+
export function classifyChangeShape(files) {
|
|
196
|
+
const code = files.filter((f) => typeof f?.path === 'string' && !isNonCodeFile(f.path));
|
|
197
|
+
if (code.length === 0)
|
|
198
|
+
return null;
|
|
199
|
+
let allComments = true;
|
|
200
|
+
const addedKeys = [];
|
|
201
|
+
const removedKeys = [];
|
|
202
|
+
for (const f of code) {
|
|
203
|
+
const patch = f.patch;
|
|
204
|
+
// ⚠ NO PATCH, NO CLAIM. GitHub omits it for binaries and oversized diffs.
|
|
205
|
+
if (typeof patch !== 'string' || patch === '')
|
|
206
|
+
return null;
|
|
207
|
+
const ext = extensionOf(f.path);
|
|
208
|
+
const singles = LINE_COMMENT[ext];
|
|
209
|
+
const blocks = BLOCK_COMMENT[ext] ?? [];
|
|
210
|
+
const hunks = hunksOf(patch);
|
|
211
|
+
if (hunks.length === 0)
|
|
212
|
+
return null;
|
|
213
|
+
for (const h of hunks) {
|
|
214
|
+
for (const l of h) {
|
|
215
|
+
if (l.marker === '+')
|
|
216
|
+
addedKeys.push(wsKey(l.text));
|
|
217
|
+
else if (l.marker === '-')
|
|
218
|
+
removedKeys.push(wsKey(l.text));
|
|
219
|
+
}
|
|
220
|
+
// An unknown language can still be `formatting` (that comparison needs no syntax), but it
|
|
221
|
+
// can never be `comments`.
|
|
222
|
+
if (allComments) {
|
|
223
|
+
if (singles == null)
|
|
224
|
+
allComments = false;
|
|
225
|
+
else if (!hunkIsCommentOnly(h, singles, blocks))
|
|
226
|
+
allComments = false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (allComments)
|
|
231
|
+
return 'comments';
|
|
232
|
+
// FORMATTING — the same lines came back, differing only in whitespace. Compared as sorted
|
|
233
|
+
// multisets so a reorder within the hunk still counts, and requiring a non-empty change so a
|
|
234
|
+
// patch with no +/- lines at all does not read as a formatting change.
|
|
235
|
+
if (addedKeys.length > 0 || removedKeys.length > 0) {
|
|
236
|
+
const a = [...addedKeys].filter((s) => s !== '').sort();
|
|
237
|
+
const b = [...removedKeys].filter((s) => s !== '').sort();
|
|
238
|
+
if (a.length === b.length && a.every((v, i) => v === b[i]))
|
|
239
|
+
return 'formatting';
|
|
240
|
+
}
|
|
241
|
+
return 'code';
|
|
242
|
+
}
|
|
243
|
+
//# sourceMappingURL=change-shape.js.map
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Pure changepoint detection over a weekly series — the unattributed half of the advisor's
|
|
2
|
+
// verification loop ("something changed around the 3rd, cause unknown"). Same robust idiom as
|
|
3
|
+
// weeklyAnomalies (queries.ts): medians + MAD, never means — a bot's weekly volume is bursty
|
|
4
|
+
// and a single spike must not manufacture a "change". No DB, no I/O, CI-tested directly.
|
|
5
|
+
//
|
|
6
|
+
// Semantics: a candidate split at index i compares values[0..i) against values[i..n). Both
|
|
7
|
+
// segments need MIN_BASELINE_POINTS non-null points or the split is unjudgeable (thin data →
|
|
8
|
+
// no claim, never a guess). The score is a robust z: |medianAfter − medianBefore| divided by
|
|
9
|
+
// the MAD-derived sigma of the POOLED residuals (each value against its own segment's
|
|
10
|
+
// median), floored at `minScale` so a near-constant series can't divide by ~0 into
|
|
11
|
+
// significance. Nulls (no-data weeks — the null-vs-zero policy) are skipped, not imputed.
|
|
12
|
+
export const CHANGEPOINT_Z = 3; // same bar as ANOMALY_Z — ≥3 robust SDs is an exception
|
|
13
|
+
export const MIN_BASELINE_POINTS = 4; // fewer and "typical" isn't yet meaningful, per side
|
|
14
|
+
function medianOf(values) {
|
|
15
|
+
if (values.length === 0)
|
|
16
|
+
return null;
|
|
17
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
18
|
+
const mid = Math.floor(sorted.length / 2);
|
|
19
|
+
return sorted.length % 2 === 1 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
20
|
+
}
|
|
21
|
+
function evaluateSplit(values, index, minScale) {
|
|
22
|
+
const before = values.slice(0, index).filter((v) => v != null);
|
|
23
|
+
const after = values.slice(index).filter((v) => v != null);
|
|
24
|
+
if (before.length < MIN_BASELINE_POINTS || after.length < MIN_BASELINE_POINTS)
|
|
25
|
+
return null;
|
|
26
|
+
const beforeMedian = medianOf(before);
|
|
27
|
+
const afterMedian = medianOf(after);
|
|
28
|
+
// Pooled residuals against each value's OWN segment median: the spread that remains once
|
|
29
|
+
// the hypothesised step is accounted for. A true step change leaves small residuals on both
|
|
30
|
+
// sides, so the z is large; noise leaves large residuals and the z collapses.
|
|
31
|
+
const residuals = [
|
|
32
|
+
...before.map((v) => Math.abs(v - beforeMedian)),
|
|
33
|
+
...after.map((v) => Math.abs(v - afterMedian)),
|
|
34
|
+
];
|
|
35
|
+
const mad = medianOf(residuals);
|
|
36
|
+
const sigma = Math.max(1.4826 * mad, minScale);
|
|
37
|
+
const z = Math.abs(afterMedian - beforeMedian) / sigma;
|
|
38
|
+
if (z < CHANGEPOINT_Z)
|
|
39
|
+
return null;
|
|
40
|
+
return {
|
|
41
|
+
index,
|
|
42
|
+
beforeMedian,
|
|
43
|
+
afterMedian,
|
|
44
|
+
direction: afterMedian > beforeMedian ? 'up' : 'down',
|
|
45
|
+
z,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Detect changepoints in a weekly series. With `anchors` (candidate split indices — e.g. the
|
|
50
|
+
* weeks of known config events) only those splits are evaluated and every significant one is
|
|
51
|
+
* returned, in index order. Without anchors, every valid split is scanned and only the single
|
|
52
|
+
* BEST split comes back (zero or one element): one series, one strongest claim — reporting
|
|
53
|
+
* every index over the bar would list the same step three times as the window slides past it.
|
|
54
|
+
*/
|
|
55
|
+
export function detectChangepoints(values, opts) {
|
|
56
|
+
if (opts.anchors) {
|
|
57
|
+
const out = [];
|
|
58
|
+
for (const index of [...opts.anchors].sort((a, b) => a - b)) {
|
|
59
|
+
if (index <= 0 || index >= values.length)
|
|
60
|
+
continue;
|
|
61
|
+
const cp = evaluateSplit(values, index, opts.minScale);
|
|
62
|
+
if (cp)
|
|
63
|
+
out.push(cp);
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
let best = null;
|
|
68
|
+
for (let index = 1; index < values.length; index++) {
|
|
69
|
+
const cp = evaluateSplit(values, index, opts.minScale);
|
|
70
|
+
if (cp && (best == null || cp.z > best.z))
|
|
71
|
+
best = cp;
|
|
72
|
+
}
|
|
73
|
+
return best ? [best] : [];
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=changepoint.js.map
|