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,398 @@
|
|
|
1
|
+
import { hasConflictMarkers } from '../coding/merge.js';
|
|
2
|
+
/**
|
|
3
|
+
* THE TWO TEXT INGRESSES' VALIDATORS. The Pro per-hunk suggestion's, and the CORE manual
|
|
4
|
+
* edit's — in one file because they are the same job (nothing untrusted becomes a blob until
|
|
5
|
+
* something here has vouched for it) and because they must not drift into two vocabularies for
|
|
6
|
+
* one fact. They are DIFFERENT functions, though, and the bottom of this file says why.
|
|
7
|
+
*
|
|
8
|
+
* THE PRO PER-HUNK SUGGESTION'S VALIDATORS — and they live in CORE, deliberately.
|
|
9
|
+
*
|
|
10
|
+
* ⚠ WHY THEY ARE NOT IN THE PLUGIN. `packages/pro/test/` does not run in CI and is not
|
|
11
|
+
* typechecked (`pnpm test` is recursive vitest and the plugin's tsconfig includes only `src`).
|
|
12
|
+
* These functions are the only thing standing between attacker-authored conflict text and a
|
|
13
|
+
* blob in somebody's repository, and they have to agree byte-for-byte with the model the host
|
|
14
|
+
* built. In core they run on every `pnpm test`, against the same `ConflictModelRegion` the land
|
|
15
|
+
* path folds.
|
|
16
|
+
*
|
|
17
|
+
* ⚠ EVERY CHECK REFUSES. There is no "accept with a warning" path and there must not be one: a
|
|
18
|
+
* suggestion that fails any check never reaches the centre pane, so the reader never sees text
|
|
19
|
+
* we could not vouch for sitting where their code will be. `cannot_reconcile` — the model saying
|
|
20
|
+
* the two sides genuinely contest — is a DESIGNED outcome on the same footing, not a failure.
|
|
21
|
+
*
|
|
22
|
+
* ⚠ THE RAW MODEL TEXT NEVER LEAVES THIS FILE'S CALLER. On a refusal the route sends the code
|
|
23
|
+
* and OUR sentence; echoing the answer back would hand an injection payload a rendering surface
|
|
24
|
+
* in the one screen whose whole job is to show text the reader is about to commit.
|
|
25
|
+
*/
|
|
26
|
+
/* ═════════════════════════════════ the fences ═════════════════════════════════ */
|
|
27
|
+
/**
|
|
28
|
+
* The six fenced regions of the prompt. Five are input the model READS; `RESOLVED` is the one it
|
|
29
|
+
* WRITES, and the only one this file extracts.
|
|
30
|
+
*
|
|
31
|
+
* ⚠ ONE PRODUCER, IN CORE, REACHED BY THE PLUGIN THROUGH THE SEAM (`ConflictSeam.fences`). The
|
|
32
|
+
* prompt's fences and the validator's extraction are the same strings by construction — a second
|
|
33
|
+
* spelling in the plugin would make every answer refuse `unparseable` against a host whose
|
|
34
|
+
* markers had moved, which is loud but pointless. Nothing here is a secret: the NONCE is what
|
|
35
|
+
* makes a fence unforgeable, not the word.
|
|
36
|
+
*/
|
|
37
|
+
export const CONFLICT_FENCE_REGIONS = [
|
|
38
|
+
'BASE',
|
|
39
|
+
'OURS',
|
|
40
|
+
'THEIRS',
|
|
41
|
+
'CONTEXT BEFORE',
|
|
42
|
+
'CONTEXT AFTER',
|
|
43
|
+
'RESOLVED',
|
|
44
|
+
];
|
|
45
|
+
/**
|
|
46
|
+
* The marker pair for every fenced region, at one per-request nonce.
|
|
47
|
+
*
|
|
48
|
+
* ⚠ THE NONCE IS THE WHOLE MECHANISM. A conflict hunk is attacker-authored on any pull request,
|
|
49
|
+
* so a line inside one reading `---END OURS---` would close a fixed fence and let the rest of
|
|
50
|
+
* that side be read as instructions. With a fresh 16-hex nonce in every marker, such a line is
|
|
51
|
+
* data: it closes nothing, and the caller re-rolls the nonce on the ~2⁻⁶⁴ chance the untrusted
|
|
52
|
+
* text contains it (see `nonceCollides`).
|
|
53
|
+
*/
|
|
54
|
+
export function conflictFences(nonce) {
|
|
55
|
+
const out = {};
|
|
56
|
+
for (const region of CONFLICT_FENCE_REGIONS) {
|
|
57
|
+
out[region] = {
|
|
58
|
+
begin: `---BEGIN ${region} ${nonce}---`,
|
|
59
|
+
end: `---END ${region} ${nonce}---`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
/** True when the nonce appears anywhere in the untrusted material, so the caller must re-roll it
|
|
65
|
+
* before building a prompt. Checked against everything that gets fenced, not just the sides. */
|
|
66
|
+
export function nonceCollides(hunk, nonce) {
|
|
67
|
+
const needle = nonce.toLowerCase();
|
|
68
|
+
for (const side of [
|
|
69
|
+
hunk.base,
|
|
70
|
+
hunk.ours,
|
|
71
|
+
hunk.theirs,
|
|
72
|
+
hunk.contextBefore,
|
|
73
|
+
hunk.contextAfter,
|
|
74
|
+
[hunk.path, hunk.headRef, hunk.baseRef],
|
|
75
|
+
]) {
|
|
76
|
+
for (const line of side)
|
|
77
|
+
if (line.toLowerCase().includes(needle))
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
/** Context lines either side of the hunk, and the per-line clip. Both are prompt budget, and
|
|
83
|
+
* clipping CONTEXT is safe in a way clipping the region is not: context is never spliced back. */
|
|
84
|
+
export const HUNK_CONTEXT_LINES = 8;
|
|
85
|
+
export const HUNK_CONTEXT_LINE_CHARS = 200;
|
|
86
|
+
/** The characters `config.conflictSuggestMaxChars` is measured against: the three SIDES only.
|
|
87
|
+
* Context is bounded by construction, so it cannot be the thing that pushes a small hunk over. */
|
|
88
|
+
export function hunkRegionChars(base, ours, theirs) {
|
|
89
|
+
let n = 0;
|
|
90
|
+
for (const side of [base, ours, theirs])
|
|
91
|
+
for (const line of side)
|
|
92
|
+
n += line.length + 1;
|
|
93
|
+
return n;
|
|
94
|
+
}
|
|
95
|
+
/* ═════════════════════════════════ validation ═════════════════════════════════ */
|
|
96
|
+
/** A lone half of a surrogate pair — legal in a JS string, not encodable as UTF-8. */
|
|
97
|
+
const LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
|
|
98
|
+
const ALPHANUMERIC = /[\p{L}\p{N}]/u;
|
|
99
|
+
/** Check 7's filter: a line substantial enough that losing it is a real loss. Punctuation-only
|
|
100
|
+
* lines are excluded HERE and deliberately included in check 8 — a dropped `}` is exactly the
|
|
101
|
+
* failure worth refusing on when the line was one side's own contribution. */
|
|
102
|
+
function significant(line) {
|
|
103
|
+
const t = line.trim();
|
|
104
|
+
return t.length >= 3 && ALPHANUMERIC.test(t);
|
|
105
|
+
}
|
|
106
|
+
function countBy(lines, keep) {
|
|
107
|
+
const m = new Map();
|
|
108
|
+
for (const line of lines) {
|
|
109
|
+
if (!keep(line))
|
|
110
|
+
continue;
|
|
111
|
+
const k = line.trim();
|
|
112
|
+
m.set(k, (m.get(k) ?? 0) + 1);
|
|
113
|
+
}
|
|
114
|
+
return m;
|
|
115
|
+
}
|
|
116
|
+
/** Trailing BLANK lines only — never leading ones, and never whitespace inside a line. A model
|
|
117
|
+
* that indents its answer is right to; a model that pads it with newlines is just verbose. */
|
|
118
|
+
function trimTrailingBlank(lines) {
|
|
119
|
+
let end = lines.length;
|
|
120
|
+
while (end > 0 && (lines[end - 1] ?? '').trim() === '')
|
|
121
|
+
end -= 1;
|
|
122
|
+
return lines.slice(0, end);
|
|
123
|
+
}
|
|
124
|
+
/** The last `n` non-blank lines of `lines`, trimmed. */
|
|
125
|
+
function tailSignificant(lines, n) {
|
|
126
|
+
const out = [];
|
|
127
|
+
for (let i = lines.length - 1; i >= 0 && out.length < n; i -= 1) {
|
|
128
|
+
const t = (lines[i] ?? '').trim();
|
|
129
|
+
if (t !== '')
|
|
130
|
+
out.unshift(t);
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
/** The first `n` non-blank lines of `lines`, trimmed. */
|
|
135
|
+
function headSignificant(lines, n) {
|
|
136
|
+
const out = [];
|
|
137
|
+
for (let i = 0; i < lines.length && out.length < n; i += 1) {
|
|
138
|
+
const t = (lines[i] ?? '').trim();
|
|
139
|
+
if (t !== '')
|
|
140
|
+
out.push(t);
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
const sameLines = (a, b) => a.length > 0 && a.length === b.length && a.every((l, i) => l === b[i]);
|
|
145
|
+
/**
|
|
146
|
+
* THE VALIDATOR. Pure — no I/O, no git, no clock. First failure wins, and every failure refuses.
|
|
147
|
+
*
|
|
148
|
+
* The order is load-bearing and pinned by a test: an answer that both kept a conflict marker and
|
|
149
|
+
* ran long reports `markers`, because the marker is the fact worth telling the reader about.
|
|
150
|
+
*
|
|
151
|
+
* 1. Shape — one `RESOLVED` fence, each marker alone on its line, in order.
|
|
152
|
+
* 2. Designed refusal — `CANNOT RECONCILE` alone in the block.
|
|
153
|
+
* 3. Text-ness — no NUL, no lone surrogate, no BOM.
|
|
154
|
+
* 4. Leakage — no nonce, no `---BEGIN `/`---END ` at a line start.
|
|
155
|
+
* 5. Markers — no surviving conflict marker.
|
|
156
|
+
* 6. Context not repeated — it must not re-emit the lines it sits between.
|
|
157
|
+
* 7. Common lines kept — what BOTH sides already had must survive.
|
|
158
|
+
* 8. Side lines kept — what EITHER side added must survive.
|
|
159
|
+
* 9. Length — bounded in lines AND bytes.
|
|
160
|
+
* 10. Not empty — when either side had content.
|
|
161
|
+
*/
|
|
162
|
+
export function validateConflictSuggestion(hunk, nonce, rawModelText) {
|
|
163
|
+
// Normalise once: CRLF → LF so a Windows-flavoured answer behaves as its LF twin, and one
|
|
164
|
+
// leading BOM stripped (a transport artefact). Any OTHER BOM is a check-3 refusal.
|
|
165
|
+
const text = rawModelText.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n');
|
|
166
|
+
const fence = conflictFences(nonce).RESOLVED;
|
|
167
|
+
const all = text.split('\n');
|
|
168
|
+
// ---- 1. Shape --------------------------------------------------------------------------
|
|
169
|
+
const begins = [];
|
|
170
|
+
const ends = [];
|
|
171
|
+
all.forEach((line, i) => {
|
|
172
|
+
const t = line.trim();
|
|
173
|
+
if (t === fence.begin)
|
|
174
|
+
begins.push(i);
|
|
175
|
+
if (t === fence.end)
|
|
176
|
+
ends.push(i);
|
|
177
|
+
});
|
|
178
|
+
const begin = begins[0];
|
|
179
|
+
const end = ends[0];
|
|
180
|
+
if (begins.length !== 1 || ends.length !== 1 || begin === undefined || end === undefined) {
|
|
181
|
+
return { ok: false, refusal: 'unparseable' };
|
|
182
|
+
}
|
|
183
|
+
if (end <= begin)
|
|
184
|
+
return { ok: false, refusal: 'unparseable' };
|
|
185
|
+
const region = trimTrailingBlank(all.slice(begin + 1, end));
|
|
186
|
+
const body = region.join('\n');
|
|
187
|
+
// ---- 2. The designed refusal -----------------------------------------------------------
|
|
188
|
+
// Its own outcome, ABOVE every content check: a model that correctly says the two sides
|
|
189
|
+
// genuinely contest must not then be told its answer was unparseable or empty.
|
|
190
|
+
if (body.trim() === 'CANNOT RECONCILE')
|
|
191
|
+
return { ok: false, refusal: 'cannot_reconcile' };
|
|
192
|
+
// ---- 3. Text-ness ----------------------------------------------------------------------
|
|
193
|
+
if (body.includes('\0') || body.includes('\uFEFF') || LONE_SURROGATE.test(body)) {
|
|
194
|
+
return { ok: false, refusal: 'not_text' };
|
|
195
|
+
}
|
|
196
|
+
// ---- 4. Leakage ------------------------------------------------------------------------
|
|
197
|
+
// The nonce inside the block means the model echoed our scaffolding; a `---BEGIN `/`---END `
|
|
198
|
+
// line start means it invented a fence of its own. Both are the same fact — the answer is not
|
|
199
|
+
// the shape we asked for — so they share `unparseable` and share one sentence on screen.
|
|
200
|
+
if (body.toLowerCase().includes(nonce.toLowerCase())) {
|
|
201
|
+
return { ok: false, refusal: 'unparseable' };
|
|
202
|
+
}
|
|
203
|
+
if (region.some((l) => l.startsWith('---BEGIN ') || l.startsWith('---END '))) {
|
|
204
|
+
return { ok: false, refusal: 'unparseable' };
|
|
205
|
+
}
|
|
206
|
+
// ---- 5. Conflict markers ---------------------------------------------------------------
|
|
207
|
+
// ⚠ IMPORTED, NOT RE-SPELT. `coding/merge.ts`'s matcher deliberately EXCLUDES a bare
|
|
208
|
+
// `=======` — real files contain rows of equals signs — and relies on the unambiguous
|
|
209
|
+
// open/close markers. A local copy here would drift from the one the merge path trusts.
|
|
210
|
+
if (hasConflictMarkers(body))
|
|
211
|
+
return { ok: false, refusal: 'markers' };
|
|
212
|
+
// ---- 6. The context is not repeated ----------------------------------------------------
|
|
213
|
+
// ⚠ NARROW ON PURPOSE, AND NOT A WHOLE-FILE COMPARISON. The context bytes are byte-identical
|
|
214
|
+
// BY CONSTRUCTION — the model is never given the chance to write them, because only the
|
|
215
|
+
// region between the two fences is ever read back. This check exists solely to catch an
|
|
216
|
+
// answer that smuggles them back INSIDE the region, which would duplicate the surrounding
|
|
217
|
+
// lines when the fold splices it. Do not "harden" it into a diff of the whole file.
|
|
218
|
+
const edge = (n) => Math.max(1, Math.min(3, n));
|
|
219
|
+
if (hunk.contextBefore.length > 0 && region.length > 0) {
|
|
220
|
+
const n = edge(Math.min(hunk.contextBefore.length, region.length));
|
|
221
|
+
if (sameLines(headSignificant(region, n), tailSignificant(hunk.contextBefore, n))) {
|
|
222
|
+
return { ok: false, refusal: 'context_duplicated' };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (hunk.contextAfter.length > 0 && region.length > 0) {
|
|
226
|
+
const n = edge(Math.min(hunk.contextAfter.length, region.length));
|
|
227
|
+
if (sameLines(tailSignificant(region, n), headSignificant(hunk.contextAfter, n))) {
|
|
228
|
+
return { ok: false, refusal: 'context_duplicated' };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// ---- 7. Lines BOTH versions kept ------------------------------------------------------
|
|
232
|
+
// The multiset intersection of the two sides' significant lines: text neither side touched
|
|
233
|
+
// and both therefore still want. Compared TRIMMED, so a whitespace-only reindent on an agreed
|
|
234
|
+
// line is not a loss.
|
|
235
|
+
const outCounts = countBy(region, () => true);
|
|
236
|
+
const oursSig = countBy(hunk.ours, significant);
|
|
237
|
+
const theirsSig = countBy(hunk.theirs, significant);
|
|
238
|
+
let keptCommonLines = 0;
|
|
239
|
+
for (const [line, n] of oursSig) {
|
|
240
|
+
const need = Math.min(n, theirsSig.get(line) ?? 0);
|
|
241
|
+
if (need === 0)
|
|
242
|
+
continue;
|
|
243
|
+
if ((outCounts.get(line) ?? 0) < need)
|
|
244
|
+
return { ok: false, refusal: 'dropped_common_lines' };
|
|
245
|
+
keptCommonLines += need;
|
|
246
|
+
}
|
|
247
|
+
// ---- 8. Lines EITHER side added -------------------------------------------------------
|
|
248
|
+
// ⚠ THE CHECK THAT MAKES 7 WORTH HAVING. Check 7 only protects text both versions already
|
|
249
|
+
// agreed on, so a model that emits THEIRS alone — deleting the pull request's entire
|
|
250
|
+
// contribution — sails through it. Here a line one side has and the ancestor does not must
|
|
251
|
+
// survive, unless the OTHER side deliberately deleted it (which is what "also in base" means
|
|
252
|
+
// once its own side's excess is accounted for).
|
|
253
|
+
//
|
|
254
|
+
// ⚠ NO ≥3-CHAR FILTER HERE, unlike check 7. A dropped `}` from one side's addition is exactly
|
|
255
|
+
// the failure worth refusing on.
|
|
256
|
+
const nonBlank = (l) => l.trim() !== '';
|
|
257
|
+
const baseAll = countBy(hunk.base, nonBlank);
|
|
258
|
+
const oursAll = countBy(hunk.ours, nonBlank);
|
|
259
|
+
const theirsAll = countBy(hunk.theirs, nonBlank);
|
|
260
|
+
const required = new Map();
|
|
261
|
+
for (const counts of [oursAll, theirsAll]) {
|
|
262
|
+
for (const [line, n] of counts) {
|
|
263
|
+
// What THIS side has beyond the ancestor. Two sides adding the same line require it
|
|
264
|
+
// ONCE, not twice — hence `max`, not a sum.
|
|
265
|
+
const excess = n - (baseAll.get(line) ?? 0);
|
|
266
|
+
if (excess <= 0)
|
|
267
|
+
continue;
|
|
268
|
+
required.set(line, Math.max(required.get(line) ?? 0, excess));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
for (const [line, need] of required) {
|
|
272
|
+
if ((outCounts.get(line) ?? 0) < need)
|
|
273
|
+
return { ok: false, refusal: 'dropped_side_lines' };
|
|
274
|
+
}
|
|
275
|
+
// ---- 9. Length -------------------------------------------------------------------------
|
|
276
|
+
const maxSideLines = Math.max(hunk.base.length, hunk.ours.length, hunk.theirs.length);
|
|
277
|
+
const sideBytes = Buffer.byteLength(hunk.ours.join('\n'), 'utf8') +
|
|
278
|
+
Buffer.byteLength(hunk.theirs.join('\n'), 'utf8');
|
|
279
|
+
if (region.length > maxSideLines * 3 + 10)
|
|
280
|
+
return { ok: false, refusal: 'too_long' };
|
|
281
|
+
if (Buffer.byteLength(body, 'utf8') > sideBytes * 2 + 512) {
|
|
282
|
+
return { ok: false, refusal: 'too_long' };
|
|
283
|
+
}
|
|
284
|
+
// ---- 10. Not empty ---------------------------------------------------------------------
|
|
285
|
+
// Reached only when checks 7 and 8 found nothing to require, which is the case where both
|
|
286
|
+
// sides are pure deletions of ancestor text — and even then an answer of nothing is a claim
|
|
287
|
+
// the reader should make themselves, not one to accept silently from a model.
|
|
288
|
+
if (region.length === 0 && (hunk.ours.length > 0 || hunk.theirs.length > 0)) {
|
|
289
|
+
return { ok: false, refusal: 'empty' };
|
|
290
|
+
}
|
|
291
|
+
return { ok: true, lines: region, keptCommonLines };
|
|
292
|
+
}
|
|
293
|
+
export const NO_EDIT_SHAPE = { crlf: false, leadingBom: false };
|
|
294
|
+
/**
|
|
295
|
+
* What an edit to this region has to inherit from it.
|
|
296
|
+
*
|
|
297
|
+
* ⚠ A TEXTAREA DESTROYS BOTH FACTS AND SAYS NOTHING. The browser's API value normalises every
|
|
298
|
+
* CRLF to a bare LF before React ever sees a keystroke (HTML's "normalize newlines"), so one
|
|
299
|
+
* character typed into a Windows-authored file used to rewrite the WHOLE hunk's line endings —
|
|
300
|
+
* a diff of every line, and a genuinely wrong file in any repo carrying `* text eol=crlf`. A BOM
|
|
301
|
+
* survives the round trip but is zero-width, so `validateConflictEdit`'s blanket U+FEFF refusal
|
|
302
|
+
* made the first region of every BOM file permanently uneditable, with a sentence
|
|
303
|
+
* ("Retype the odd one out") naming a character nobody can see.
|
|
304
|
+
*
|
|
305
|
+
* @param sides the region's own `base`, `ours` and `theirs` line texts, terminators attached.
|
|
306
|
+
* @param isFirstRegion is this the file's FIRST region? Only there can a BOM be legitimate.
|
|
307
|
+
*/
|
|
308
|
+
export function editShapeFor(sides, isFirstRegion) {
|
|
309
|
+
const lines = sides.flat();
|
|
310
|
+
return {
|
|
311
|
+
// ⚠ UNANIMOUS, NOT A MAJORITY. A CRLF file's final line carries no `\r` when the file has no
|
|
312
|
+
// trailing newline, so that region reads as mixed and is left alone — which is correct: there
|
|
313
|
+
// is nothing there to re-impose without inventing a byte.
|
|
314
|
+
crlf: lines.length > 0 && lines.every((l) => l.endsWith('\r')),
|
|
315
|
+
leadingBom: isFirstRegion && sides.some((side) => side[0]?.startsWith('') === true),
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* THE MANUAL EDIT'S VALIDATOR — three checks, and the list of what it deliberately does NOT
|
|
320
|
+
* check is the more important half.
|
|
321
|
+
*
|
|
322
|
+
* 1. Text-ness — no NUL, no lone surrogate, no BOM.
|
|
323
|
+
* 2. Markers — no surviving conflict marker.
|
|
324
|
+
* 3. Length — bounded by `config.conflictSuggestMaxChars`, ONE budget for both ingresses.
|
|
325
|
+
*
|
|
326
|
+
* ⚠ 1 IS NOT A FORMALITY HERE. `land.ts` does `Buffer.from(foldToText(...), 'utf8')`, and its
|
|
327
|
+
* byte-for-byte round-trip claim rests on every side having decoded STRICTLY as UTF-8 at model
|
|
328
|
+
* build — which is why `not_text` is a file-level REFUSAL there rather than a lossy decode.
|
|
329
|
+
* Typed text does not inherit that provenance: a lone surrogate reaches `Buffer.from` and is
|
|
330
|
+
* silently substituted with U+FFFD, so the bytes committed would not be the bytes anybody saw.
|
|
331
|
+
*
|
|
332
|
+
* ⚠ THE U+FEFF HALF OF 1 IS NOT BLANKET, AND THE EXCEPTION IS THE FILE'S OWN BOM. `model.ts`
|
|
333
|
+
* decodes with `ignoreBOM: true` deliberately, so a BOM file's first line really does begin
|
|
334
|
+
* U+FEFF and the textarea really is seeded with it. Refusing that made the first region of every
|
|
335
|
+
* Visual-Studio-authored file permanently uneditable — and the only escape, retyping the hunk
|
|
336
|
+
* from scratch, silently stripped the BOM that the decoder exists to preserve. So when
|
|
337
|
+
* `shape.leadingBom` says this region owns the file's BOM, one LEADING U+FEFF is taken off before
|
|
338
|
+
* the scan and put back on the first stored line. Anywhere else it is still `not_text`, and
|
|
339
|
+
* `validateConflictSuggestion` — whose input is MODEL output, where a BOM is junk — is untouched.
|
|
340
|
+
*
|
|
341
|
+
* ⚠ AND THE LINE ENDING IS RE-IMPOSED, BECAUSE THE BROWSER ALREADY DESTROYED IT. See
|
|
342
|
+
* `editShapeFor`: a textarea hands back LF for every CRLF it was given, so without this one
|
|
343
|
+
* keystroke in a CRLF file rewrites the whole hunk's endings. Imposing costs nothing when the
|
|
344
|
+
* region is LF (`shape.crlf` false ⇒ this is a no-op) and is the only way the stored lines can
|
|
345
|
+
* agree with their own neighbours.
|
|
346
|
+
*
|
|
347
|
+
* ⚠ 2 IS NOT COVERED BY ANYTHING ELSE. The land path's full-resolution guard only proves each
|
|
348
|
+
* conflicted path was OVERWRITTEN (merge-tree's tree stores marker content at those paths); it
|
|
349
|
+
* never inspects the bytes. A typed `<<<<<<<` would commit with nothing refusing it.
|
|
350
|
+
*
|
|
351
|
+
* ⚠ AND CHECKS 6, 7 AND 8 OF THE SUGGESTION VALIDATOR ARE DELIBERATELY ABSENT — context not
|
|
352
|
+
* repeated, common lines kept, side lines kept. Those three exist because a MODEL silently drops
|
|
353
|
+
* lines it was not asked to drop, and the reader has no way to know it happened. A PERSON
|
|
354
|
+
* deleting a line is not a failure mode, it is the feature: the whole reason this route exists
|
|
355
|
+
* is the trailing comma, the duplicated import, the brace that belongs to neither side. Porting
|
|
356
|
+
* them over would refuse exactly the edits somebody opened the textarea to make.
|
|
357
|
+
*
|
|
358
|
+
* The order is text-ness → markers → length, matching the suggestion validator's: the marker is
|
|
359
|
+
* the fact worth telling somebody about, and "this is also too long" can wait. The scans cannot
|
|
360
|
+
* run away on a huge body because the route's ajv schema bounds `text` structurally first.
|
|
361
|
+
*/
|
|
362
|
+
export function validateConflictEdit(text, maxChars, shape = NO_EDIT_SHAPE) {
|
|
363
|
+
// The file's own BOM, off the front before the scan and back on after it. Only ever ONE, only
|
|
364
|
+
// ever leading, and only when this region is where the file's BOM lives.
|
|
365
|
+
const bom = shape.leadingBom && text.startsWith('\uFEFF');
|
|
366
|
+
const body = bom ? text.slice(1) : text;
|
|
367
|
+
if (body.includes('\0') || body.includes('\uFEFF') || LONE_SURROGATE.test(body)) {
|
|
368
|
+
return { ok: false, refusal: 'not_text' };
|
|
369
|
+
}
|
|
370
|
+
if (hasConflictMarkers(body))
|
|
371
|
+
return { ok: false, refusal: 'markers' };
|
|
372
|
+
if (body.length > maxChars)
|
|
373
|
+
return { ok: false, refusal: 'too_long' };
|
|
374
|
+
const lines = splitEditLines(body).map((l) => shape.crlf && !l.endsWith('\r') ? `${l}\r` : l);
|
|
375
|
+
// \u26A0 RE-ATTACHED TO THE FIRST LINE, NOT TO THE TEXT. The reader may have deleted it \u2014 it is
|
|
376
|
+
// zero-width, so they cannot have done so deliberately \u2014 and the file's decoder is documented
|
|
377
|
+
// to keep it. An edit that leaves the region EMPTY has no first line, and in that case the BOM
|
|
378
|
+
// goes with the lines the reader deleted, which is the one place they really did choose.
|
|
379
|
+
if (shape.leadingBom && lines[0] != null && !lines[0].startsWith('\uFEFF')) {
|
|
380
|
+
lines[0] = `\uFEFF${lines[0]}`;
|
|
381
|
+
}
|
|
382
|
+
return { ok: true, lines };
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* The reader's text as fold lines.
|
|
386
|
+
*
|
|
387
|
+
* ⚠ AN EMPTY TEXTAREA IS ZERO LINES, NOT ONE BLANK ONE. `''.split('\n')` is `['']`, which folds
|
|
388
|
+
* to a region containing one empty line — not what somebody who selected everything and pressed
|
|
389
|
+
* delete asked for. Deleting a whole hunk has to be reachable; it is half of why this exists.
|
|
390
|
+
*
|
|
391
|
+
* ⚠ A TRAILING NEWLINE IS A REAL TRAILING BLANK LINE, and stays one. The suggestion validator
|
|
392
|
+
* trims trailing blanks because a verbose model pads its answer; a person who left a blank line
|
|
393
|
+
* at the end of the box can see it there and meant it.
|
|
394
|
+
*/
|
|
395
|
+
function splitEditLines(text) {
|
|
396
|
+
return text === '' ? [] : text.split('\n');
|
|
397
|
+
}
|
|
398
|
+
//# sourceMappingURL=suggestion.js.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tokenisers + the interner the diff runs on.
|
|
3
|
+
*
|
|
4
|
+
* Everything the resolver diffs — lines at file grain, words inside one contested region — is
|
|
5
|
+
* turned into an array of small integers first. The diff then compares numbers, which is what
|
|
6
|
+
* makes the O(ND) inner loop cheap enough to run over a whole file and again over every
|
|
7
|
+
* conflicted region.
|
|
8
|
+
*
|
|
9
|
+
* ⚠ `tokenizeWords(text).join('') === text` is a HARD invariant, not a nicety. The wand's
|
|
10
|
+
* word-level merge reassembles a region by concatenating token runs, so a tokeniser that
|
|
11
|
+
* dropped or normalised a byte would silently commit different text than the user read.
|
|
12
|
+
* `tokens.test.ts` pins it over code, CRLF, tabs, unicode identifiers and emoji.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Split into: runs of word characters, runs of whitespace, and one code point for anything
|
|
16
|
+
* else. Unicode-aware on both counts — `ünicode` is ONE token, and an astral code point (an
|
|
17
|
+
* emoji, a rare CJK ideograph) is one token rather than two lone surrogates.
|
|
18
|
+
*/
|
|
19
|
+
const WORD_TOKEN_RE = /[\p{L}\p{N}_]+|\s+|[\s\S]/gu;
|
|
20
|
+
export function tokenizeWords(text) {
|
|
21
|
+
return text.match(WORD_TOKEN_RE) ?? [];
|
|
22
|
+
}
|
|
23
|
+
/** True when a token is entirely whitespace. The disjointness rule needs REAL evidence that
|
|
24
|
+
* two authors worked on different things, and a single space between two edits is not it. */
|
|
25
|
+
export function isWhitespaceToken(token) {
|
|
26
|
+
return token.length > 0 && token.trim().length === 0;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Map token strings to dense integer ids. One interner per diff pair, so ids are only ever
|
|
30
|
+
* compared within the arrays that produced them.
|
|
31
|
+
*/
|
|
32
|
+
export class TokenInterner {
|
|
33
|
+
ids = new Map();
|
|
34
|
+
id(token) {
|
|
35
|
+
const existing = this.ids.get(token);
|
|
36
|
+
if (existing !== undefined)
|
|
37
|
+
return existing;
|
|
38
|
+
const next = this.ids.size;
|
|
39
|
+
this.ids.set(token, next);
|
|
40
|
+
return next;
|
|
41
|
+
}
|
|
42
|
+
/** How many distinct tokens have been seen. */
|
|
43
|
+
get size() {
|
|
44
|
+
return this.ids.size;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Intern a whole array in one pass. */
|
|
48
|
+
export function internAll(tokens, interner) {
|
|
49
|
+
const out = new Array(tokens.length);
|
|
50
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
51
|
+
// noUncheckedIndexedAccess: the loop bound guarantees it, the compiler does not.
|
|
52
|
+
out[i] = interner.id(tokens[i]);
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Split a UTF-8 string into lines WITHOUT their terminators, plus whether the text ended with
|
|
58
|
+
* one. `\r` stays attached to its line, so a CRLF file round-trips byte for byte.
|
|
59
|
+
*
|
|
60
|
+
* An empty file is zero lines and no terminator — NOT one empty line, which would commit a
|
|
61
|
+
* newline into a file that never had one.
|
|
62
|
+
*/
|
|
63
|
+
export function splitLines(text) {
|
|
64
|
+
if (text.length === 0)
|
|
65
|
+
return { lines: [], finalNewline: false };
|
|
66
|
+
const finalNewline = text.endsWith('\n');
|
|
67
|
+
const lines = text.split('\n');
|
|
68
|
+
if (finalNewline)
|
|
69
|
+
lines.pop();
|
|
70
|
+
return { lines, finalNewline };
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=tokens.js.map
|
package/dist/data-dir.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// THE LOCAL DATA DIRECTORY — ~/.limn, and the one-time move from ~/.pierre-review.
|
|
2
|
+
//
|
|
3
|
+
// Everything Limn keeps on a user's machine lives under ONE directory: the installed CLI's SQLite
|
|
4
|
+
// database, the clone cache, `config.json` (the per-review budget) and the AI runtime the agentic
|
|
5
|
+
// features download on first use (`ai-runtime/`, see ai/runtime.ts). Before the npm package was
|
|
6
|
+
// renamed `pierre-review` → `limn-review` that directory was `~/.pierre-review`.
|
|
7
|
+
//
|
|
8
|
+
// ⚠ THE MOVE IS ONE `rename`, AND IT NEVER CLOBBERS. On the first local boot after the upgrade an
|
|
9
|
+
// existing `~/.pierre-review` is renamed to `~/.limn` — DB, WAL files, clones, config, everything,
|
|
10
|
+
// in one atomic step on the same filesystem (both live in the home directory). If `~/.limn`
|
|
11
|
+
// already exists nothing is moved and nothing is deleted: the old directory is simply left where
|
|
12
|
+
// it is. A copy-then-delete would be the dangerous version (a half-copied SQLite file next to a
|
|
13
|
+
// deleted original), which is why there is none.
|
|
14
|
+
//
|
|
15
|
+
// ⚠ AN EXPLICIT LOCATION WINS AND IS NEVER MIGRATED INTO. `LIMN_DATA_DIR` names the directory; when
|
|
16
|
+
// it is set this module moves nothing, because the user chose where their data lives. The finer
|
|
17
|
+
// overrides keep working on top of either: `DATABASE_URL` / `--db` (the database file) and
|
|
18
|
+
// `CLONE_DIR` (the clone cache).
|
|
19
|
+
//
|
|
20
|
+
// ⚠ A PINNED PATH INSIDE THE OLD DIRECTORY STOPS THE MOVE. The old README told people to pass
|
|
21
|
+
// `--db ~/.pierre-review/pierre-review.sqlite`, so a shell profile may still say so. Moving the
|
|
22
|
+
// directory out from under that value would make the next open CREATE an empty database at the old
|
|
23
|
+
// path while the real one sat unseen in ~/.limn. So nothing moves (`pinned_path`), and everything
|
|
24
|
+
// keeps using the old directory (below).
|
|
25
|
+
//
|
|
26
|
+
// ⚠ UNTIL THE MOVE HAS HAPPENED, THE OLD DIRECTORY IS THE DATA DIRECTORY. `resolveDataDir` answers
|
|
27
|
+
// `~/.pierre-review` while it exists and `~/.limn` does not — after a pinned path, or a rename that
|
|
28
|
+
// failed (Windows EBUSY on an open SQLite file, EXDEV, EACCES). Answering `~/.limn` there would
|
|
29
|
+
// create it, fresh and empty, and from then on every boot would stop at `target_exists` with the
|
|
30
|
+
// user's history stranded. Instead the next boot simply tries the move again.
|
|
31
|
+
//
|
|
32
|
+
// No imports outside `node:*`, so cli.ts can load it before config.ts reads the environment.
|
|
33
|
+
import { existsSync, renameSync } from 'node:fs';
|
|
34
|
+
import { homedir } from 'node:os';
|
|
35
|
+
import { isAbsolute, join, relative, resolve } from 'node:path';
|
|
36
|
+
/** The directory name under the home directory. */
|
|
37
|
+
export const DATA_DIR_NAME = '.limn';
|
|
38
|
+
/** The pre-rename directory name, moved once on first boot. */
|
|
39
|
+
export const LEGACY_DATA_DIR_NAME = '.pierre-review';
|
|
40
|
+
/**
|
|
41
|
+
* The local data directory: `LIMN_DATA_DIR` when set; else `~/.pierre-review` while it exists and
|
|
42
|
+
* `~/.limn` does not (the move has not happened yet — see the header); else `~/.limn`.
|
|
43
|
+
*/
|
|
44
|
+
export function resolveDataDir(env = process.env, home = homedir()) {
|
|
45
|
+
const explicit = env.LIMN_DATA_DIR?.trim();
|
|
46
|
+
if (explicit)
|
|
47
|
+
return resolve(explicit);
|
|
48
|
+
const target = join(home, DATA_DIR_NAME);
|
|
49
|
+
const legacy = join(home, LEGACY_DATA_DIR_NAME);
|
|
50
|
+
if (existsSync(legacy) && !existsSync(target))
|
|
51
|
+
return legacy;
|
|
52
|
+
return target;
|
|
53
|
+
}
|
|
54
|
+
/** The database file the CLI and the server open by default (the name predates the rename). */
|
|
55
|
+
const DB_FILE = 'pierre-review.sqlite';
|
|
56
|
+
function isInside(dir, path) {
|
|
57
|
+
const rel = relative(dir, path);
|
|
58
|
+
return rel === '' || (!!rel && !rel.startsWith('..') && !isAbsolute(rel));
|
|
59
|
+
}
|
|
60
|
+
// The same failure can be reported by the CLI's call and the server's; say it once per process.
|
|
61
|
+
const said = new Set();
|
|
62
|
+
/**
|
|
63
|
+
* Move `~/.pierre-review` to `~/.limn`, once. Safe to call on every boot: after the first move
|
|
64
|
+
* there is no legacy directory left, so it is a single `existsSync`.
|
|
65
|
+
*/
|
|
66
|
+
export function migrateLegacyDataDir(opts = {}) {
|
|
67
|
+
const env = opts.env ?? process.env;
|
|
68
|
+
const home = opts.home ?? homedir();
|
|
69
|
+
const say = (msg) => {
|
|
70
|
+
if (said.has(msg))
|
|
71
|
+
return;
|
|
72
|
+
said.add(msg);
|
|
73
|
+
opts.log?.(msg);
|
|
74
|
+
};
|
|
75
|
+
if (env.LIMN_DATA_DIR?.trim())
|
|
76
|
+
return { moved: false, reason: 'explicit_dir' };
|
|
77
|
+
const from = join(home, LEGACY_DATA_DIR_NAME);
|
|
78
|
+
const to = join(home, DATA_DIR_NAME);
|
|
79
|
+
if (!existsSync(from))
|
|
80
|
+
return { moved: false, reason: 'no_legacy_dir' };
|
|
81
|
+
// Never clobber: an existing ~/.limn is the user's data too. The old directory stays as it is —
|
|
82
|
+
// but a ~/.limn with no database next to an old one that has one is worth a line.
|
|
83
|
+
if (existsSync(to)) {
|
|
84
|
+
if (!existsSync(join(to, DB_FILE)) && existsSync(join(from, DB_FILE))) {
|
|
85
|
+
say(`Your earlier Limn data is still in ${from}, and ${to} already exists, so it was not moved. To use it, stop Limn and move ${join(from, DB_FILE)} into ${to}.`);
|
|
86
|
+
}
|
|
87
|
+
return { moved: false, reason: 'target_exists' };
|
|
88
|
+
}
|
|
89
|
+
// A database or clone path pinned inside the old directory: moving it would orphan that path.
|
|
90
|
+
const resolvePath = opts.resolvePath ?? ((p) => resolve(p));
|
|
91
|
+
for (const key of ['DATABASE_URL', 'CLONE_DIR']) {
|
|
92
|
+
const raw = env[key]?.trim();
|
|
93
|
+
if (raw && isInside(from, resolvePath(raw))) {
|
|
94
|
+
say(`Your data stays in ${from} because ${key} points inside it. To move it to ${to}, stop Limn, move the folder, and update ${key}.`);
|
|
95
|
+
return { moved: false, reason: 'pinned_path' };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
renameSync(from, to);
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
// Leave everything where it was. `resolveDataDir` keeps answering the old directory while
|
|
103
|
+
// ~/.limn does not exist, so this boot runs on the user's real data and the next one tries
|
|
104
|
+
// the move again.
|
|
105
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
106
|
+
say(`Could not move ${from} to ${to} (${error}). Limn is using ${from} for now and will try again next time.`);
|
|
107
|
+
return { moved: false, reason: 'rename_failed', error };
|
|
108
|
+
}
|
|
109
|
+
say(`Moved your Limn data from ${from} to ${to}.`);
|
|
110
|
+
return { moved: true, from, to };
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=data-dir.js.map
|