@newrelic/preflight 0.0.1-pre.1 → 1.0.0
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 +183 -0
- package/README.md +498 -0
- package/dist/alerts/alert-log.d.ts +24 -0
- package/dist/alerts/alert-log.d.ts.map +1 -0
- package/dist/alerts/alert-log.js +159 -0
- package/dist/alerts/alert-log.js.map +1 -0
- package/dist/alerts/alert-snapshot-collector.d.ts +168 -0
- package/dist/alerts/alert-snapshot-collector.d.ts.map +1 -0
- package/dist/alerts/alert-snapshot-collector.js +243 -0
- package/dist/alerts/alert-snapshot-collector.js.map +1 -0
- package/dist/alerts/local-alert-engine.d.ts +86 -0
- package/dist/alerts/local-alert-engine.d.ts.map +1 -0
- package/dist/alerts/local-alert-engine.js +466 -0
- package/dist/alerts/local-alert-engine.js.map +1 -0
- package/dist/alerts/local-alert-rule.d.ts +439 -0
- package/dist/alerts/local-alert-rule.d.ts.map +1 -0
- package/dist/alerts/local-alert-rule.js +139 -0
- package/dist/alerts/local-alert-rule.js.map +1 -0
- package/dist/alerts/os-notifier.d.ts +39 -0
- package/dist/alerts/os-notifier.d.ts.map +1 -0
- package/dist/alerts/os-notifier.js +170 -0
- package/dist/alerts/os-notifier.js.map +1 -0
- package/dist/alerts/types.d.ts +35 -0
- package/dist/alerts/types.d.ts.map +1 -0
- package/dist/alerts/types.js +8 -0
- package/dist/alerts/types.js.map +1 -0
- package/dist/config.d.ts +169 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +860 -0
- package/dist/config.js.map +1 -0
- package/dist/dashboard/dashboard-server.d.ts +38 -0
- package/dist/dashboard/dashboard-server.d.ts.map +1 -0
- package/dist/dashboard/dashboard-server.js +207 -0
- package/dist/dashboard/dashboard-server.js.map +1 -0
- package/dist/dashboard/index.d.ts +3 -0
- package/dist/dashboard/index.d.ts.map +1 -0
- package/dist/dashboard/index.js +2 -0
- package/dist/dashboard/index.js.map +1 -0
- package/dist/dashboard/live-event-bus.d.ts +99 -0
- package/dist/dashboard/live-event-bus.d.ts.map +1 -0
- package/dist/dashboard/live-event-bus.js +56 -0
- package/dist/dashboard/live-event-bus.js.map +1 -0
- package/dist/dashboard/routes/api-handler.d.ts +122 -0
- package/dist/dashboard/routes/api-handler.d.ts.map +1 -0
- package/dist/dashboard/routes/api-handler.js +1414 -0
- package/dist/dashboard/routes/api-handler.js.map +1 -0
- package/dist/dashboard/routes/replay-analyzer.d.ts +15 -0
- package/dist/dashboard/routes/replay-analyzer.d.ts.map +1 -0
- package/dist/dashboard/routes/replay-analyzer.js +227 -0
- package/dist/dashboard/routes/replay-analyzer.js.map +1 -0
- package/dist/dashboard/routes/sse-handler.d.ts +4 -0
- package/dist/dashboard/routes/sse-handler.d.ts.map +1 -0
- package/dist/dashboard/routes/sse-handler.js +122 -0
- package/dist/dashboard/routes/sse-handler.js.map +1 -0
- package/dist/dashboard/routes/static-handler.d.ts +3 -0
- package/dist/dashboard/routes/static-handler.d.ts.map +1 -0
- package/dist/dashboard/routes/static-handler.js +103 -0
- package/dist/dashboard/routes/static-handler.js.map +1 -0
- package/dist/data/alerts/conditions/01-daily-cost-spike.json +16 -0
- package/dist/data/alerts/conditions/02-low-efficiency-score.json +16 -0
- package/dist/data/alerts/conditions/03-stuck-loop-rate.json +16 -0
- package/dist/data/alerts/conditions/04-anti-pattern-rate.json +16 -0
- package/dist/data/alerts/conditions/05-session-cost-budget.json +16 -0
- package/dist/data/alerts/conditions-personal/01-personal-daily-cost.json +16 -0
- package/dist/data/alerts/conditions-personal/02-personal-session-cost.json +16 -0
- package/dist/data/alerts/conditions-personal/03-personal-low-efficiency.json +16 -0
- package/dist/data/alerts/conditions-personal/04-personal-anti-pattern-rate.json +16 -0
- package/dist/data/alerts/conditions-personal/05-personal-stuck-loop.json +16 -0
- package/dist/data/alerts/policy.json +4 -0
- package/dist/data/dashboards/ai-coding-assistant-manager-view.json +103 -0
- package/dist/data/dashboards/ai-coding-assistant-overview.json +239 -0
- package/dist/data/dashboards/ai-coding-assistant-personal.json +442 -0
- package/dist/data/dashboards/ai-coding-assistant-platform-comparison.json +320 -0
- package/dist/data/dashboards/ai-coding-assistant-security.json +275 -0
- package/dist/data/dashboards/ai-coding-assistant-session-detail.json +296 -0
- package/dist/data/dashboards/ai-coding-assistant-team-view.json +345 -0
- package/dist/deploy/data-paths.d.ts +22 -0
- package/dist/deploy/data-paths.d.ts.map +1 -0
- package/dist/deploy/data-paths.js +69 -0
- package/dist/deploy/data-paths.js.map +1 -0
- package/dist/deploy/deploy-alerts.d.ts +58 -0
- package/dist/deploy/deploy-alerts.d.ts.map +1 -0
- package/dist/deploy/deploy-alerts.js +371 -0
- package/dist/deploy/deploy-alerts.js.map +1 -0
- package/dist/deploy/deploy-dashboards.d.ts +92 -0
- package/dist/deploy/deploy-dashboards.d.ts.map +1 -0
- package/dist/deploy/deploy-dashboards.js +282 -0
- package/dist/deploy/deploy-dashboards.js.map +1 -0
- package/dist/digest/digest-formatter.d.ts +3 -0
- package/dist/digest/digest-formatter.d.ts.map +1 -0
- package/dist/digest/digest-formatter.js +37 -0
- package/dist/digest/digest-formatter.js.map +1 -0
- package/dist/digest/digest-sender.d.ts +2 -0
- package/dist/digest/digest-sender.d.ts.map +1 -0
- package/dist/digest/digest-sender.js +29 -0
- package/dist/digest/digest-sender.js.map +1 -0
- package/dist/hooks/bash-classifier.d.ts +26 -0
- package/dist/hooks/bash-classifier.d.ts.map +1 -0
- package/dist/hooks/bash-classifier.js +409 -0
- package/dist/hooks/bash-classifier.js.map +1 -0
- package/dist/hooks/collector-script.d.ts +47 -0
- package/dist/hooks/collector-script.d.ts.map +1 -0
- package/dist/hooks/collector-script.js +662 -0
- package/dist/hooks/collector-script.js.map +1 -0
- package/dist/hooks/event-processor.d.ts +65 -0
- package/dist/hooks/event-processor.d.ts.map +1 -0
- package/dist/hooks/event-processor.js +342 -0
- package/dist/hooks/event-processor.js.map +1 -0
- package/dist/hooks/index.d.ts +7 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +5 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/session-resolver.d.ts +66 -0
- package/dist/hooks/session-resolver.d.ts.map +1 -0
- package/dist/hooks/session-resolver.js +196 -0
- package/dist/hooks/session-resolver.js.map +1 -0
- package/dist/hooks/tool-parsers.d.ts +19 -0
- package/dist/hooks/tool-parsers.d.ts.map +1 -0
- package/dist/hooks/tool-parsers.js +260 -0
- package/dist/hooks/tool-parsers.js.map +1 -0
- package/dist/index.d.ts +107 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1505 -0
- package/dist/index.js.map +1 -0
- package/dist/install/cli.d.ts +11 -0
- package/dist/install/cli.d.ts.map +1 -0
- package/dist/install/cli.js +365 -0
- package/dist/install/cli.js.map +1 -0
- package/dist/install/index.d.ts +4 -0
- package/dist/install/index.d.ts.map +1 -0
- package/dist/install/index.js +3 -0
- package/dist/install/index.js.map +1 -0
- package/dist/install/install-helper.d.ts +35 -0
- package/dist/install/install-helper.d.ts.map +1 -0
- package/dist/install/install-helper.js +227 -0
- package/dist/install/install-helper.js.map +1 -0
- package/dist/install/key-validator.d.ts +19 -0
- package/dist/install/key-validator.d.ts.map +1 -0
- package/dist/install/key-validator.js +122 -0
- package/dist/install/key-validator.js.map +1 -0
- package/dist/install/migrate.d.ts +12 -0
- package/dist/install/migrate.d.ts.map +1 -0
- package/dist/install/migrate.js +115 -0
- package/dist/install/migrate.js.map +1 -0
- package/dist/install/schedule.d.ts +11 -0
- package/dist/install/schedule.d.ts.map +1 -0
- package/dist/install/schedule.js +114 -0
- package/dist/install/schedule.js.map +1 -0
- package/dist/install/setup-wizard.d.ts +40 -0
- package/dist/install/setup-wizard.d.ts.map +1 -0
- package/dist/install/setup-wizard.js +489 -0
- package/dist/install/setup-wizard.js.map +1 -0
- package/dist/lib/date.d.ts +54 -0
- package/dist/lib/date.d.ts.map +1 -0
- package/dist/lib/date.js +85 -0
- package/dist/lib/date.js.map +1 -0
- package/dist/metrics/anti-patterns.d.ts +62 -0
- package/dist/metrics/anti-patterns.d.ts.map +1 -0
- package/dist/metrics/anti-patterns.js +301 -0
- package/dist/metrics/anti-patterns.js.map +1 -0
- package/dist/metrics/api-failure-tracker.d.ts +82 -0
- package/dist/metrics/api-failure-tracker.d.ts.map +1 -0
- package/dist/metrics/api-failure-tracker.js +202 -0
- package/dist/metrics/api-failure-tracker.js.map +1 -0
- package/dist/metrics/budget-tracker.d.ts +60 -0
- package/dist/metrics/budget-tracker.d.ts.map +1 -0
- package/dist/metrics/budget-tracker.js +130 -0
- package/dist/metrics/budget-tracker.js.map +1 -0
- package/dist/metrics/claudemd-tracker.d.ts +108 -0
- package/dist/metrics/claudemd-tracker.d.ts.map +1 -0
- package/dist/metrics/claudemd-tracker.js +337 -0
- package/dist/metrics/claudemd-tracker.js.map +1 -0
- package/dist/metrics/collaboration-profile.d.ts +65 -0
- package/dist/metrics/collaboration-profile.d.ts.map +1 -0
- package/dist/metrics/collaboration-profile.js +231 -0
- package/dist/metrics/collaboration-profile.js.map +1 -0
- package/dist/metrics/context-composition-tracker.d.ts +74 -0
- package/dist/metrics/context-composition-tracker.d.ts.map +1 -0
- package/dist/metrics/context-composition-tracker.js +202 -0
- package/dist/metrics/context-composition-tracker.js.map +1 -0
- package/dist/metrics/context-tracker.d.ts +78 -0
- package/dist/metrics/context-tracker.d.ts.map +1 -0
- package/dist/metrics/context-tracker.js +222 -0
- package/dist/metrics/context-tracker.js.map +1 -0
- package/dist/metrics/context-window-tracker.d.ts +18 -0
- package/dist/metrics/context-window-tracker.d.ts.map +1 -0
- package/dist/metrics/context-window-tracker.js +35 -0
- package/dist/metrics/context-window-tracker.js.map +1 -0
- package/dist/metrics/cost-forecast.d.ts +36 -0
- package/dist/metrics/cost-forecast.d.ts.map +1 -0
- package/dist/metrics/cost-forecast.js +91 -0
- package/dist/metrics/cost-forecast.js.map +1 -0
- package/dist/metrics/cost-per-outcome.d.ts +102 -0
- package/dist/metrics/cost-per-outcome.d.ts.map +1 -0
- package/dist/metrics/cost-per-outcome.js +266 -0
- package/dist/metrics/cost-per-outcome.js.map +1 -0
- package/dist/metrics/cost-tracker.d.ts +78 -0
- package/dist/metrics/cost-tracker.d.ts.map +1 -0
- package/dist/metrics/cost-tracker.js +169 -0
- package/dist/metrics/cost-tracker.js.map +1 -0
- package/dist/metrics/decision-tracker.d.ts +49 -0
- package/dist/metrics/decision-tracker.d.ts.map +1 -0
- package/dist/metrics/decision-tracker.js +161 -0
- package/dist/metrics/decision-tracker.js.map +1 -0
- package/dist/metrics/efficiency-score.d.ts +80 -0
- package/dist/metrics/efficiency-score.d.ts.map +1 -0
- package/dist/metrics/efficiency-score.js +219 -0
- package/dist/metrics/efficiency-score.js.map +1 -0
- package/dist/metrics/git-efficiency-tracker.d.ts +165 -0
- package/dist/metrics/git-efficiency-tracker.d.ts.map +1 -0
- package/dist/metrics/git-efficiency-tracker.js +1056 -0
- package/dist/metrics/git-efficiency-tracker.js.map +1 -0
- package/dist/metrics/index.d.ts +26 -0
- package/dist/metrics/index.d.ts.map +1 -0
- package/dist/metrics/index.js +14 -0
- package/dist/metrics/index.js.map +1 -0
- package/dist/metrics/instruction-drift-tracker.d.ts +69 -0
- package/dist/metrics/instruction-drift-tracker.d.ts.map +1 -0
- package/dist/metrics/instruction-drift-tracker.js +213 -0
- package/dist/metrics/instruction-drift-tracker.js.map +1 -0
- package/dist/metrics/latency-decomposition.d.ts +50 -0
- package/dist/metrics/latency-decomposition.d.ts.map +1 -0
- package/dist/metrics/latency-decomposition.js +112 -0
- package/dist/metrics/latency-decomposition.js.map +1 -0
- package/dist/metrics/latency-tracker.d.ts +33 -0
- package/dist/metrics/latency-tracker.d.ts.map +1 -0
- package/dist/metrics/latency-tracker.js +93 -0
- package/dist/metrics/latency-tracker.js.map +1 -0
- package/dist/metrics/live-session-registry.d.ts +29 -0
- package/dist/metrics/live-session-registry.d.ts.map +1 -0
- package/dist/metrics/live-session-registry.js +103 -0
- package/dist/metrics/live-session-registry.js.map +1 -0
- package/dist/metrics/model-usage-tracker.d.ts +21 -0
- package/dist/metrics/model-usage-tracker.d.ts.map +1 -0
- package/dist/metrics/model-usage-tracker.js +53 -0
- package/dist/metrics/model-usage-tracker.js.map +1 -0
- package/dist/metrics/percentile.d.ts +5 -0
- package/dist/metrics/percentile.d.ts.map +1 -0
- package/dist/metrics/percentile.js +10 -0
- package/dist/metrics/percentile.js.map +1 -0
- package/dist/metrics/personal-coach.d.ts +47 -0
- package/dist/metrics/personal-coach.d.ts.map +1 -0
- package/dist/metrics/personal-coach.js +241 -0
- package/dist/metrics/personal-coach.js.map +1 -0
- package/dist/metrics/prompt-feedback.d.ts +75 -0
- package/dist/metrics/prompt-feedback.d.ts.map +1 -0
- package/dist/metrics/prompt-feedback.js +286 -0
- package/dist/metrics/prompt-feedback.js.map +1 -0
- package/dist/metrics/proxy-metrics.d.ts +54 -0
- package/dist/metrics/proxy-metrics.d.ts.map +1 -0
- package/dist/metrics/proxy-metrics.js +228 -0
- package/dist/metrics/proxy-metrics.js.map +1 -0
- package/dist/metrics/quality-proxy-tracker.d.ts +51 -0
- package/dist/metrics/quality-proxy-tracker.d.ts.map +1 -0
- package/dist/metrics/quality-proxy-tracker.js +162 -0
- package/dist/metrics/quality-proxy-tracker.js.map +1 -0
- package/dist/metrics/recommendation-engine.d.ts +72 -0
- package/dist/metrics/recommendation-engine.d.ts.map +1 -0
- package/dist/metrics/recommendation-engine.js +207 -0
- package/dist/metrics/recommendation-engine.js.map +1 -0
- package/dist/metrics/retry-detector.d.ts +43 -0
- package/dist/metrics/retry-detector.d.ts.map +1 -0
- package/dist/metrics/retry-detector.js +179 -0
- package/dist/metrics/retry-detector.js.map +1 -0
- package/dist/metrics/session-tracker.d.ts +75 -0
- package/dist/metrics/session-tracker.d.ts.map +1 -0
- package/dist/metrics/session-tracker.js +249 -0
- package/dist/metrics/session-tracker.js.map +1 -0
- package/dist/metrics/task-completion-tracker.d.ts +15 -0
- package/dist/metrics/task-completion-tracker.d.ts.map +1 -0
- package/dist/metrics/task-completion-tracker.js +27 -0
- package/dist/metrics/task-completion-tracker.js.map +1 -0
- package/dist/metrics/task-detector.d.ts +84 -0
- package/dist/metrics/task-detector.d.ts.map +1 -0
- package/dist/metrics/task-detector.js +302 -0
- package/dist/metrics/task-detector.js.map +1 -0
- package/dist/metrics/tool-selection-scorer.d.ts +39 -0
- package/dist/metrics/tool-selection-scorer.d.ts.map +1 -0
- package/dist/metrics/tool-selection-scorer.js +193 -0
- package/dist/metrics/tool-selection-scorer.js.map +1 -0
- package/dist/metrics/trend-analyzer.d.ts +92 -0
- package/dist/metrics/trend-analyzer.d.ts.map +1 -0
- package/dist/metrics/trend-analyzer.js +293 -0
- package/dist/metrics/trend-analyzer.js.map +1 -0
- package/dist/metrics/turn-cost-attributor.d.ts +41 -0
- package/dist/metrics/turn-cost-attributor.d.ts.map +1 -0
- package/dist/metrics/turn-cost-attributor.js +118 -0
- package/dist/metrics/turn-cost-attributor.js.map +1 -0
- package/dist/metrics/turn-tracker.d.ts +49 -0
- package/dist/metrics/turn-tracker.d.ts.map +1 -0
- package/dist/metrics/turn-tracker.js +192 -0
- package/dist/metrics/turn-tracker.js.map +1 -0
- package/dist/platforms/amazon-q-adapter.d.ts +10 -0
- package/dist/platforms/amazon-q-adapter.d.ts.map +1 -0
- package/dist/platforms/amazon-q-adapter.js +75 -0
- package/dist/platforms/amazon-q-adapter.js.map +1 -0
- package/dist/platforms/claude-code-adapter.d.ts +10 -0
- package/dist/platforms/claude-code-adapter.d.ts.map +1 -0
- package/dist/platforms/claude-code-adapter.js +48 -0
- package/dist/platforms/claude-code-adapter.js.map +1 -0
- package/dist/platforms/continue-adapter.d.ts +10 -0
- package/dist/platforms/continue-adapter.d.ts.map +1 -0
- package/dist/platforms/continue-adapter.js +73 -0
- package/dist/platforms/continue-adapter.js.map +1 -0
- package/dist/platforms/copilot-adapter.d.ts +37 -0
- package/dist/platforms/copilot-adapter.d.ts.map +1 -0
- package/dist/platforms/copilot-adapter.js +66 -0
- package/dist/platforms/copilot-adapter.js.map +1 -0
- package/dist/platforms/cursor-adapter.d.ts +10 -0
- package/dist/platforms/cursor-adapter.d.ts.map +1 -0
- package/dist/platforms/cursor-adapter.js +60 -0
- package/dist/platforms/cursor-adapter.js.map +1 -0
- package/dist/platforms/generic-mcp-adapter.d.ts +113 -0
- package/dist/platforms/generic-mcp-adapter.d.ts.map +1 -0
- package/dist/platforms/generic-mcp-adapter.js +139 -0
- package/dist/platforms/generic-mcp-adapter.js.map +1 -0
- package/dist/platforms/index.d.ts +15 -0
- package/dist/platforms/index.d.ts.map +1 -0
- package/dist/platforms/index.js +12 -0
- package/dist/platforms/index.js.map +1 -0
- package/dist/platforms/platform-registry.d.ts +11 -0
- package/dist/platforms/platform-registry.d.ts.map +1 -0
- package/dist/platforms/platform-registry.js +54 -0
- package/dist/platforms/platform-registry.js.map +1 -0
- package/dist/platforms/types.d.ts +36 -0
- package/dist/platforms/types.d.ts.map +1 -0
- package/dist/platforms/types.js +2 -0
- package/dist/platforms/types.js.map +1 -0
- package/dist/platforms/windsurf-adapter.d.ts +10 -0
- package/dist/platforms/windsurf-adapter.d.ts.map +1 -0
- package/dist/platforms/windsurf-adapter.js +63 -0
- package/dist/platforms/windsurf-adapter.js.map +1 -0
- package/dist/platforms/zed-adapter.d.ts +10 -0
- package/dist/platforms/zed-adapter.d.ts.map +1 -0
- package/dist/platforms/zed-adapter.js +72 -0
- package/dist/platforms/zed-adapter.js.map +1 -0
- package/dist/proxy/index.d.ts +7 -0
- package/dist/proxy/index.d.ts.map +1 -0
- package/dist/proxy/index.js +5 -0
- package/dist/proxy/index.js.map +1 -0
- package/dist/proxy/otlp-receiver.d.ts +28 -0
- package/dist/proxy/otlp-receiver.d.ts.map +1 -0
- package/dist/proxy/otlp-receiver.js +319 -0
- package/dist/proxy/otlp-receiver.js.map +1 -0
- package/dist/proxy/proxy-manager.d.ts +47 -0
- package/dist/proxy/proxy-manager.d.ts.map +1 -0
- package/dist/proxy/proxy-manager.js +338 -0
- package/dist/proxy/proxy-manager.js.map +1 -0
- package/dist/proxy/types.d.ts +72 -0
- package/dist/proxy/types.d.ts.map +1 -0
- package/dist/proxy/types.js +33 -0
- package/dist/proxy/types.js.map +1 -0
- package/dist/proxy/upstream-http.d.ts +26 -0
- package/dist/proxy/upstream-http.d.ts.map +1 -0
- package/dist/proxy/upstream-http.js +209 -0
- package/dist/proxy/upstream-http.js.map +1 -0
- package/dist/proxy/upstream-stdio.d.ts +25 -0
- package/dist/proxy/upstream-stdio.d.ts.map +1 -0
- package/dist/proxy/upstream-stdio.js +256 -0
- package/dist/proxy/upstream-stdio.js.map +1 -0
- package/dist/security/audit-trail.d.ts +74 -0
- package/dist/security/audit-trail.d.ts.map +1 -0
- package/dist/security/audit-trail.js +338 -0
- package/dist/security/audit-trail.js.map +1 -0
- package/dist/security/index.d.ts +5 -0
- package/dist/security/index.d.ts.map +1 -0
- package/dist/security/index.js +4 -0
- package/dist/security/index.js.map +1 -0
- package/dist/security/ssrf.d.ts +2 -0
- package/dist/security/ssrf.d.ts.map +1 -0
- package/dist/security/ssrf.js +126 -0
- package/dist/security/ssrf.js.map +1 -0
- package/dist/server.d.ts +14 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +117 -0
- package/dist/server.js.map +1 -0
- package/dist/shared/__test-utils__/log-output.d.ts +49 -0
- package/dist/shared/__test-utils__/log-output.d.ts.map +1 -0
- package/dist/shared/__test-utils__/log-output.js +38 -0
- package/dist/shared/__test-utils__/log-output.js.map +1 -0
- package/dist/shared/config.d.ts +56 -0
- package/dist/shared/config.d.ts.map +1 -0
- package/dist/shared/config.js +290 -0
- package/dist/shared/config.js.map +1 -0
- package/dist/shared/errors.d.ts +139 -0
- package/dist/shared/errors.d.ts.map +1 -0
- package/dist/shared/errors.js +406 -0
- package/dist/shared/errors.js.map +1 -0
- package/dist/shared/events/factory.d.ts +143 -0
- package/dist/shared/events/factory.d.ts.map +1 -0
- package/dist/shared/events/factory.js +351 -0
- package/dist/shared/events/factory.js.map +1 -0
- package/dist/shared/events/index.d.ts +6 -0
- package/dist/shared/events/index.d.ts.map +1 -0
- package/dist/shared/events/index.js +3 -0
- package/dist/shared/events/index.js.map +1 -0
- package/dist/shared/events/serialize.d.ts +87 -0
- package/dist/shared/events/serialize.d.ts.map +1 -0
- package/dist/shared/events/serialize.js +510 -0
- package/dist/shared/events/serialize.js.map +1 -0
- package/dist/shared/events/types.d.ts +139 -0
- package/dist/shared/events/types.d.ts.map +1 -0
- package/dist/shared/events/types.js +2 -0
- package/dist/shared/events/types.js.map +1 -0
- package/dist/shared/harvest/event-buffer.d.ts +59 -0
- package/dist/shared/harvest/event-buffer.d.ts.map +1 -0
- package/dist/shared/harvest/event-buffer.js +100 -0
- package/dist/shared/harvest/event-buffer.js.map +1 -0
- package/dist/shared/harvest/harvest-scheduler.d.ts +200 -0
- package/dist/shared/harvest/harvest-scheduler.d.ts.map +1 -0
- package/dist/shared/harvest/harvest-scheduler.js +647 -0
- package/dist/shared/harvest/harvest-scheduler.js.map +1 -0
- package/dist/shared/harvest/index.d.ts +7 -0
- package/dist/shared/harvest/index.d.ts.map +1 -0
- package/dist/shared/harvest/index.js +4 -0
- package/dist/shared/harvest/index.js.map +1 -0
- package/dist/shared/harvest/metric-aggregator.d.ts +115 -0
- package/dist/shared/harvest/metric-aggregator.d.ts.map +1 -0
- package/dist/shared/harvest/metric-aggregator.js +247 -0
- package/dist/shared/harvest/metric-aggregator.js.map +1 -0
- package/dist/shared/index.d.ts +22 -0
- package/dist/shared/index.d.ts.map +1 -0
- package/dist/shared/index.js +13 -0
- package/dist/shared/index.js.map +1 -0
- package/dist/shared/logger.d.ts +57 -0
- package/dist/shared/logger.d.ts.map +1 -0
- package/dist/shared/logger.js +166 -0
- package/dist/shared/logger.js.map +1 -0
- package/dist/shared/pricing-data.d.ts +4 -0
- package/dist/shared/pricing-data.d.ts.map +1 -0
- package/dist/shared/pricing-data.js +473 -0
- package/dist/shared/pricing-data.js.map +1 -0
- package/dist/shared/pricing.d.ts +148 -0
- package/dist/shared/pricing.d.ts.map +1 -0
- package/dist/shared/pricing.js +528 -0
- package/dist/shared/pricing.js.map +1 -0
- package/dist/shared/redact.d.ts +33 -0
- package/dist/shared/redact.d.ts.map +1 -0
- package/dist/shared/redact.js +110 -0
- package/dist/shared/redact.js.map +1 -0
- package/dist/shared/timing.d.ts +96 -0
- package/dist/shared/timing.d.ts.map +1 -0
- package/dist/shared/timing.js +173 -0
- package/dist/shared/timing.js.map +1 -0
- package/dist/shared/tokens.d.ts +145 -0
- package/dist/shared/tokens.d.ts.map +1 -0
- package/dist/shared/tokens.js +492 -0
- package/dist/shared/tokens.js.map +1 -0
- package/dist/shared/transport/events-api.d.ts +14 -0
- package/dist/shared/transport/events-api.d.ts.map +1 -0
- package/dist/shared/transport/events-api.js +29 -0
- package/dist/shared/transport/events-api.js.map +1 -0
- package/dist/shared/transport/http-client.d.ts +49 -0
- package/dist/shared/transport/http-client.d.ts.map +1 -0
- package/dist/shared/transport/http-client.js +381 -0
- package/dist/shared/transport/http-client.js.map +1 -0
- package/dist/shared/transport/index.d.ts +10 -0
- package/dist/shared/transport/index.d.ts.map +1 -0
- package/dist/shared/transport/index.js +6 -0
- package/dist/shared/transport/index.js.map +1 -0
- package/dist/shared/transport/logs-api.d.ts +29 -0
- package/dist/shared/transport/logs-api.d.ts.map +1 -0
- package/dist/shared/transport/logs-api.js +40 -0
- package/dist/shared/transport/logs-api.js.map +1 -0
- package/dist/shared/transport/metric-api.d.ts +9 -0
- package/dist/shared/transport/metric-api.d.ts.map +1 -0
- package/dist/shared/transport/metric-api.js +39 -0
- package/dist/shared/transport/metric-api.js.map +1 -0
- package/dist/shared/transport/otlp-event-bridge.d.ts +22 -0
- package/dist/shared/transport/otlp-event-bridge.d.ts.map +1 -0
- package/dist/shared/transport/otlp-event-bridge.js +50 -0
- package/dist/shared/transport/otlp-event-bridge.js.map +1 -0
- package/dist/shared/transport/otlp-shared.d.ts +14 -0
- package/dist/shared/transport/otlp-shared.d.ts.map +1 -0
- package/dist/shared/transport/otlp-shared.js +49 -0
- package/dist/shared/transport/otlp-shared.js.map +1 -0
- package/dist/shared/transport/otlp-transport.d.ts +58 -0
- package/dist/shared/transport/otlp-transport.d.ts.map +1 -0
- package/dist/shared/transport/otlp-transport.js +236 -0
- package/dist/shared/transport/otlp-transport.js.map +1 -0
- package/dist/shared/transport/types.d.ts +129 -0
- package/dist/shared/transport/types.d.ts.map +1 -0
- package/dist/shared/transport/types.js +2 -0
- package/dist/shared/transport/types.js.map +1 -0
- package/dist/shared/version.d.ts +2 -0
- package/dist/shared/version.d.ts.map +1 -0
- package/dist/shared/version.js +2 -0
- package/dist/shared/version.js.map +1 -0
- package/dist/storage/index.d.ts +7 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +4 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/local-store.d.ts +153 -0
- package/dist/storage/local-store.d.ts.map +1 -0
- package/dist/storage/local-store.js +719 -0
- package/dist/storage/local-store.js.map +1 -0
- package/dist/storage/retention.d.ts +2 -0
- package/dist/storage/retention.d.ts.map +1 -0
- package/dist/storage/retention.js +53 -0
- package/dist/storage/retention.js.map +1 -0
- package/dist/storage/session-store.d.ts +97 -0
- package/dist/storage/session-store.d.ts.map +1 -0
- package/dist/storage/session-store.js +391 -0
- package/dist/storage/session-store.js.map +1 -0
- package/dist/storage/types.d.ts +64 -0
- package/dist/storage/types.d.ts.map +1 -0
- package/dist/storage/types.js +2 -0
- package/dist/storage/types.js.map +1 -0
- package/dist/storage/weekly-summary.d.ts +61 -0
- package/dist/storage/weekly-summary.d.ts.map +1 -0
- package/dist/storage/weekly-summary.js +243 -0
- package/dist/storage/weekly-summary.js.map +1 -0
- package/dist/tools/analytics-tools.d.ts +101 -0
- package/dist/tools/analytics-tools.d.ts.map +1 -0
- package/dist/tools/analytics-tools.js +71 -0
- package/dist/tools/analytics-tools.js.map +1 -0
- package/dist/tools/cost-tools.d.ts +121 -0
- package/dist/tools/cost-tools.d.ts.map +1 -0
- package/dist/tools/cost-tools.js +174 -0
- package/dist/tools/cost-tools.js.map +1 -0
- package/dist/tools/cross-session-tools.d.ts +376 -0
- package/dist/tools/cross-session-tools.d.ts.map +1 -0
- package/dist/tools/cross-session-tools.js +820 -0
- package/dist/tools/cross-session-tools.js.map +1 -0
- package/dist/tools/extended-analytics-tools.d.ts +164 -0
- package/dist/tools/extended-analytics-tools.d.ts.map +1 -0
- package/dist/tools/extended-analytics-tools.js +121 -0
- package/dist/tools/extended-analytics-tools.js.map +1 -0
- package/dist/tools/index.d.ts +7 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +4 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/session-stats.d.ts +162 -0
- package/dist/tools/session-stats.d.ts.map +1 -0
- package/dist/tools/session-stats.js +1054 -0
- package/dist/tools/session-stats.js.map +1 -0
- package/dist/tools/workflow-tools.d.ts +126 -0
- package/dist/tools/workflow-tools.d.ts.map +1 -0
- package/dist/tools/workflow-tools.js +274 -0
- package/dist/tools/workflow-tools.js.map +1 -0
- package/dist/tracing/mcp-tracer.d.ts +4 -0
- package/dist/tracing/mcp-tracer.d.ts.map +1 -0
- package/dist/tracing/mcp-tracer.js +14 -0
- package/dist/tracing/mcp-tracer.js.map +1 -0
- package/dist/tracing/session-span.d.ts +14 -0
- package/dist/tracing/session-span.d.ts.map +1 -0
- package/dist/tracing/session-span.js +53 -0
- package/dist/tracing/session-span.js.map +1 -0
- package/dist/tracing/task-span-tracker.d.ts +11 -0
- package/dist/tracing/task-span-tracker.d.ts.map +1 -0
- package/dist/tracing/task-span-tracker.js +59 -0
- package/dist/tracing/task-span-tracker.js.map +1 -0
- package/dist/tracing/tool-call-span.d.ts +4 -0
- package/dist/tracing/tool-call-span.d.ts.map +1 -0
- package/dist/tracing/tool-call-span.js +60 -0
- package/dist/tracing/tool-call-span.js.map +1 -0
- package/dist/transport/index.d.ts +3 -0
- package/dist/transport/index.d.ts.map +1 -0
- package/dist/transport/index.js +2 -0
- package/dist/transport/index.js.map +1 -0
- package/dist/transport/log-ingest.d.ts +42 -0
- package/dist/transport/log-ingest.d.ts.map +1 -0
- package/dist/transport/log-ingest.js +151 -0
- package/dist/transport/log-ingest.js.map +1 -0
- package/dist/transport/nr-ingest.d.ts +171 -0
- package/dist/transport/nr-ingest.d.ts.map +1 -0
- package/dist/transport/nr-ingest.js +659 -0
- package/dist/transport/nr-ingest.js.map +1 -0
- package/dist/types.d.ts +45 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/web/assets/index-BrL281N-.css +2 -0
- package/dist/web/assets/index-CcaYZzXm.js +42 -0
- package/dist/web/favicon.svg +15 -0
- package/dist/web/index.html +15 -0
- package/examples/local-alert-rules.json +106 -0
- package/package.json +125 -1
|
@@ -0,0 +1,1056 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Git command classification patterns
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
const GIT_COMMAND_RE = /\bgit\s+/;
|
|
5
|
+
const MERGE_CONFLICT_INDICATORS = [
|
|
6
|
+
/CONFLICT\s*\(/i,
|
|
7
|
+
/Automatic merge failed/i,
|
|
8
|
+
/fix conflicts and then commit/i,
|
|
9
|
+
/Merge conflict in/i,
|
|
10
|
+
/both modified:/i,
|
|
11
|
+
];
|
|
12
|
+
const REBASE_CONFLICT_RE = /\brebase\b.*(?:conflict|could not apply|patch does not apply)/i;
|
|
13
|
+
const MERGE_ABORT_RE = /\bgit\s+merge\s+--abort\b/;
|
|
14
|
+
const REBASE_ABORT_RE = /\bgit\s+rebase\s+--abort\b/;
|
|
15
|
+
const CHERRY_PICK_ABORT_RE = /\bgit\s+cherry-pick\s+--abort\b/;
|
|
16
|
+
const GIT_PULL_RE = /\bgit\s+pull\b/;
|
|
17
|
+
const GIT_FETCH_RE = /\bgit\s+fetch\b/;
|
|
18
|
+
const GIT_PUSH_RE = /\bgit\s+push\b/;
|
|
19
|
+
const GIT_PUSH_FORCE_RE = /\bgit\s+push\s+.*--force(?!-)|\bgit\s+push\s+-f\b/;
|
|
20
|
+
const GIT_PUSH_FORCE_LEASE_RE = /--force-with-lease\b/;
|
|
21
|
+
const GIT_MERGE_RE = /\bgit\s+merge\b/;
|
|
22
|
+
const GIT_REBASE_RE = /\bgit\s+rebase\b/;
|
|
23
|
+
const GIT_STASH_RE = /\bgit\s+stash\b/;
|
|
24
|
+
const GIT_RESET_HARD_RE = /\bgit\s+reset\s+--hard\b/;
|
|
25
|
+
const GIT_CHECKOUT_DASH_RE = /\bgit\s+checkout\s+--\s/;
|
|
26
|
+
const GIT_RESTORE_RE = /\bgit\s+restore\b/;
|
|
27
|
+
const GIT_BRANCH_RE = /\bgit\s+(?:branch|checkout\s+-b|switch\s+-c)\b/;
|
|
28
|
+
const GIT_STATUS_RE = /\bgit\s+status\b/;
|
|
29
|
+
const GIT_DIFF_RE = /\bgit\s+diff\b/;
|
|
30
|
+
const GIT_LOG_RE = /\bgit\s+log\b/;
|
|
31
|
+
const GIT_COMMIT_RE = /\bgit\s+commit\b/;
|
|
32
|
+
const GIT_WORKTREE_RE = /\bgit\s+worktree\b/;
|
|
33
|
+
const GIT_CHECKOUT_OURS_RE = /\bgit\s+checkout\s+--ours\b/;
|
|
34
|
+
const GIT_CHECKOUT_THEIRS_RE = /\bgit\s+checkout\s+--theirs\b/;
|
|
35
|
+
const GIT_CHERRY_PICK_RE = /\bgit\s+cherry-pick\b/;
|
|
36
|
+
// GitHub CLI patterns
|
|
37
|
+
const GH_PR_CREATE_RE = /\bgh\s+pr\s+create\b/;
|
|
38
|
+
const GH_PR_MERGE_RE = /\bgh\s+pr\s+merge\b/;
|
|
39
|
+
const GH_PR_VIEW_RE = /\bgh\s+pr\s+view\b/;
|
|
40
|
+
const GH_PR_EDIT_RE = /\bgh\s+pr\s+edit\b/;
|
|
41
|
+
const GH_PR_READY_RE = /\bgh\s+pr\s+ready\b/;
|
|
42
|
+
const GH_PR_CHECKS_RE = /\bgh\s+pr\s+checks\b/;
|
|
43
|
+
const GH_COMMAND_RE = /\bgh\s+/;
|
|
44
|
+
// Extract PR number from gh commands
|
|
45
|
+
const GH_PR_NUMBER_RE = /\bgh\s+pr\s+\w+\s+(\d+)/;
|
|
46
|
+
const REJECT_INDICATORS = [
|
|
47
|
+
/\[rejected\]/i,
|
|
48
|
+
/non-fast-forward/i,
|
|
49
|
+
/failed to push/i,
|
|
50
|
+
/Updates were rejected/i,
|
|
51
|
+
];
|
|
52
|
+
// Conflict file path extraction: "CONFLICT (content): Merge conflict in <path>"
|
|
53
|
+
const CONFLICT_FILE_RE = /Merge conflict in (.+)/g;
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Tracker
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
export class GitEfficiencyTracker {
|
|
58
|
+
events = [];
|
|
59
|
+
conflictRecords = [];
|
|
60
|
+
pendingConflictTimestamp = null;
|
|
61
|
+
pendingConflictCommand = '';
|
|
62
|
+
pendingConflictFiles = [];
|
|
63
|
+
lastSyncTimestamp = null;
|
|
64
|
+
pullsSinceLastConflict = 0;
|
|
65
|
+
consecutiveFailedPushes = 0;
|
|
66
|
+
statusChecksSinceLastAction = 0;
|
|
67
|
+
firstEditTimestamp = null;
|
|
68
|
+
firstSyncTimestamp = null;
|
|
69
|
+
commitsSinceLastSync = 0;
|
|
70
|
+
syncIntervalCommitCounts = [];
|
|
71
|
+
pushRejections = 0;
|
|
72
|
+
forceAfterReject = 0;
|
|
73
|
+
lastPushRejectedTimestamp = null;
|
|
74
|
+
conflictedFiles = new Set();
|
|
75
|
+
editedFiles = new Set();
|
|
76
|
+
hasUsedWorktree = false;
|
|
77
|
+
hasUsedForceWithLease = false;
|
|
78
|
+
totalToolCalls = 0;
|
|
79
|
+
sessionStartTimestamp = null;
|
|
80
|
+
commitTimestamps = [];
|
|
81
|
+
worktreeCommands = 0;
|
|
82
|
+
oursCount = 0;
|
|
83
|
+
theirsCount = 0;
|
|
84
|
+
cherryPickCount = 0;
|
|
85
|
+
lastBuildOrTestTimestamp = null;
|
|
86
|
+
lastPushTimestamp = null;
|
|
87
|
+
buildBeforePush = null;
|
|
88
|
+
commitsAheadOfMain = null;
|
|
89
|
+
commitsBehindMain = null;
|
|
90
|
+
quickConflictResolutions = 0;
|
|
91
|
+
prEvents = [];
|
|
92
|
+
firstCommitTimestamp = null;
|
|
93
|
+
repoContext = {
|
|
94
|
+
repoName: null,
|
|
95
|
+
branch: null,
|
|
96
|
+
remoteName: null,
|
|
97
|
+
defaultBranch: null,
|
|
98
|
+
};
|
|
99
|
+
recordToolCall(record) {
|
|
100
|
+
this.totalToolCalls++;
|
|
101
|
+
if (this.sessionStartTimestamp === null) {
|
|
102
|
+
this.sessionStartTimestamp = record.timestamp;
|
|
103
|
+
}
|
|
104
|
+
// Track file edits (non-git tool calls that modify files)
|
|
105
|
+
if (record.toolName === 'Edit' || record.toolName === 'Write') {
|
|
106
|
+
const filePath = record.filePath;
|
|
107
|
+
if (filePath) {
|
|
108
|
+
this.editedFiles.add(filePath);
|
|
109
|
+
if (this.firstEditTimestamp === null) {
|
|
110
|
+
this.firstEditTimestamp = record.timestamp;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Track build/test commands for "verify before push" metric
|
|
115
|
+
if (record.isTestCommand || record.isBuildCommand) {
|
|
116
|
+
this.lastBuildOrTestTimestamp = record.timestamp;
|
|
117
|
+
}
|
|
118
|
+
const command = record.command;
|
|
119
|
+
if (!command)
|
|
120
|
+
return;
|
|
121
|
+
// Track GitHub CLI PR commands (skip if `git commit` is the *command*, not
|
|
122
|
+
// text that happens to appear inside a gh argument like --title).
|
|
123
|
+
if (GH_COMMAND_RE.test(command) && !command.trimStart().startsWith('git ')) {
|
|
124
|
+
this.processGhCommand(command, record.timestamp);
|
|
125
|
+
}
|
|
126
|
+
if (!GIT_COMMAND_RE.test(command))
|
|
127
|
+
return;
|
|
128
|
+
const event = this.classifyGitCommand(command, record);
|
|
129
|
+
this.events.push(event);
|
|
130
|
+
this.processEvent(event, command, record);
|
|
131
|
+
}
|
|
132
|
+
hydrateBranchDivergence(ahead, behind) {
|
|
133
|
+
this.commitsAheadOfMain = ahead;
|
|
134
|
+
this.commitsBehindMain = behind;
|
|
135
|
+
}
|
|
136
|
+
hydrateRepoContext(ctx) {
|
|
137
|
+
this.repoContext = ctx;
|
|
138
|
+
}
|
|
139
|
+
processGhCommand(command, timestamp) {
|
|
140
|
+
const numberMatch = GH_PR_NUMBER_RE.exec(command);
|
|
141
|
+
const prNumber = numberMatch ? numberMatch[1] : null;
|
|
142
|
+
if (GH_PR_CREATE_RE.test(command)) {
|
|
143
|
+
this.prEvents.push({ timestamp, action: 'create', prNumber });
|
|
144
|
+
}
|
|
145
|
+
else if (GH_PR_MERGE_RE.test(command)) {
|
|
146
|
+
this.prEvents.push({ timestamp, action: 'merge', prNumber });
|
|
147
|
+
}
|
|
148
|
+
else if (GH_PR_CHECKS_RE.test(command)) {
|
|
149
|
+
this.prEvents.push({ timestamp, action: 'checks', prNumber });
|
|
150
|
+
}
|
|
151
|
+
else if (GH_PR_READY_RE.test(command)) {
|
|
152
|
+
this.prEvents.push({ timestamp, action: 'ready', prNumber });
|
|
153
|
+
}
|
|
154
|
+
else if (GH_PR_EDIT_RE.test(command)) {
|
|
155
|
+
this.prEvents.push({ timestamp, action: 'edit', prNumber });
|
|
156
|
+
}
|
|
157
|
+
else if (GH_PR_VIEW_RE.test(command)) {
|
|
158
|
+
this.prEvents.push({ timestamp, action: 'view', prNumber });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
computePrMetrics() {
|
|
162
|
+
const created = this.prEvents.filter((e) => e.action === 'create').length;
|
|
163
|
+
const merged = this.prEvents.filter((e) => e.action === 'merge').length;
|
|
164
|
+
const checksViewed = this.prEvents.filter((e) => e.action === 'checks').length;
|
|
165
|
+
const prsUpdated = this.prEvents.filter((e) => e.action === 'edit' || e.action === 'ready').length;
|
|
166
|
+
// Time from first commit to first PR creation
|
|
167
|
+
let avgTimeToCreateMs = null;
|
|
168
|
+
if (created > 0 && this.firstCommitTimestamp !== null) {
|
|
169
|
+
const firstCreate = this.prEvents.find((e) => e.action === 'create');
|
|
170
|
+
if (firstCreate) {
|
|
171
|
+
avgTimeToCreateMs = Math.max(0, firstCreate.timestamp - this.firstCommitTimestamp);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
created,
|
|
176
|
+
merged,
|
|
177
|
+
checksViewed,
|
|
178
|
+
prsUpdated,
|
|
179
|
+
prActivity: this.prEvents.slice(-20),
|
|
180
|
+
avgTimeToCreateMs,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
hydrateGitLog(commits) {
|
|
184
|
+
for (const commit of commits) {
|
|
185
|
+
if (!commit.hash)
|
|
186
|
+
continue;
|
|
187
|
+
const event = {
|
|
188
|
+
timestamp: commit.timestamp,
|
|
189
|
+
type: 'commit',
|
|
190
|
+
command: `git commit (${commit.hash})`,
|
|
191
|
+
success: true,
|
|
192
|
+
durationMs: null,
|
|
193
|
+
};
|
|
194
|
+
// Only add if we don't already have this commit tracked
|
|
195
|
+
const isDuplicate = this.events.some((e) => e.type === 'commit' && e.command?.includes(commit.hash));
|
|
196
|
+
if (!isDuplicate) {
|
|
197
|
+
this.events.push(event);
|
|
198
|
+
this.commitTimestamps.push(commit.timestamp);
|
|
199
|
+
// Don't increment commitsSinceLastSync for historical commits — this counter
|
|
200
|
+
// tracks real-time session activity, not replayed history.
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
replayTimeline(entries) {
|
|
205
|
+
for (const entry of entries) {
|
|
206
|
+
const syntheticRecord = {
|
|
207
|
+
id: `replay-${entry.timestamp}`,
|
|
208
|
+
sessionId: null,
|
|
209
|
+
toolName: entry.toolName,
|
|
210
|
+
toolUseId: `replay-${entry.timestamp}`,
|
|
211
|
+
timestamp: entry.timestamp,
|
|
212
|
+
durationMs: entry.durationMs,
|
|
213
|
+
success: entry.success,
|
|
214
|
+
command: entry.command,
|
|
215
|
+
filePath: entry.filePath,
|
|
216
|
+
errorType: entry.errorType,
|
|
217
|
+
};
|
|
218
|
+
this.recordToolCall(syntheticRecord);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
getMetrics() {
|
|
222
|
+
const totalGitCommands = this.events.length;
|
|
223
|
+
const mergeConflicts = this.events.filter((e) => e.type === 'merge_conflict').length;
|
|
224
|
+
const rebaseConflicts = this.events.filter((e) => e.type === 'rebase_conflict').length;
|
|
225
|
+
const abortedOperations = this.events.filter((e) => e.type === 'merge_abort' || e.type === 'rebase_abort' || e.type === 'cherry_pick_abort').length;
|
|
226
|
+
const forcePushes = this.events.filter((e) => e.type === 'force_push' || e.type === 'force_push_lease').length;
|
|
227
|
+
const resetHards = this.events.filter((e) => e.type === 'reset_hard').length;
|
|
228
|
+
const discardedChanges = this.events.filter((e) => e.type === 'discard_changes').length;
|
|
229
|
+
const pullCount = this.events.filter((e) => e.type === 'pull').length;
|
|
230
|
+
const pushCount = this.events.filter((e) => e.type === 'push' || e.type === 'force_push' || e.type === 'force_push_lease').length;
|
|
231
|
+
const commitCount = this.events.filter((e) => e.type === 'commit').length;
|
|
232
|
+
const branchOperations = this.events.filter((e) => e.type === 'branch').length;
|
|
233
|
+
const resolved = this.conflictRecords.filter((c) => c.resolution === 'resolved');
|
|
234
|
+
const conflictResolutionRate = this.conflictRecords.length > 0 ? resolved.length / this.conflictRecords.length : null;
|
|
235
|
+
const resolutionTimes = resolved
|
|
236
|
+
.filter((c) => c.resolutionTimeMs !== null)
|
|
237
|
+
.map((c) => c.resolutionTimeMs);
|
|
238
|
+
const avgConflictResolutionMs = resolutionTimes.length > 0
|
|
239
|
+
? resolutionTimes.reduce((a, b) => a + b, 0) / resolutionTimes.length
|
|
240
|
+
: null;
|
|
241
|
+
const staleBranchPulls = this.countStaleBranchPulls();
|
|
242
|
+
const riskIndicators = this.computeRiskIndicators();
|
|
243
|
+
const suggestions = this.generateSuggestions({
|
|
244
|
+
totalGitCommands,
|
|
245
|
+
mergeConflicts,
|
|
246
|
+
rebaseConflicts,
|
|
247
|
+
abortedOperations,
|
|
248
|
+
forcePushes,
|
|
249
|
+
resetHards,
|
|
250
|
+
discardedChanges,
|
|
251
|
+
pullCount,
|
|
252
|
+
commitCount,
|
|
253
|
+
staleBranchPulls,
|
|
254
|
+
riskIndicators,
|
|
255
|
+
});
|
|
256
|
+
const bestPractices = this.evaluateBestPractices(riskIndicators, {
|
|
257
|
+
mergeConflicts,
|
|
258
|
+
rebaseConflicts,
|
|
259
|
+
commitCount,
|
|
260
|
+
pullCount,
|
|
261
|
+
forcePushes,
|
|
262
|
+
});
|
|
263
|
+
const efficiencyScore = this.computeScore({
|
|
264
|
+
totalGitCommands,
|
|
265
|
+
mergeConflicts,
|
|
266
|
+
rebaseConflicts,
|
|
267
|
+
abortedOperations,
|
|
268
|
+
forcePushes,
|
|
269
|
+
resetHards,
|
|
270
|
+
discardedChanges,
|
|
271
|
+
conflictResolutionRate,
|
|
272
|
+
});
|
|
273
|
+
const preventionScore = this.computePreventionScore(bestPractices);
|
|
274
|
+
const velocityMetrics = this.computeVelocityMetrics();
|
|
275
|
+
const conflictResolutionStrategy = this.computeConflictStrategy();
|
|
276
|
+
const prMetrics = this.computePrMetrics();
|
|
277
|
+
return {
|
|
278
|
+
totalGitCommands,
|
|
279
|
+
mergeConflicts,
|
|
280
|
+
rebaseConflicts,
|
|
281
|
+
abortedOperations,
|
|
282
|
+
forcePushes,
|
|
283
|
+
resetHards,
|
|
284
|
+
discardedChanges,
|
|
285
|
+
pullCount,
|
|
286
|
+
pushCount,
|
|
287
|
+
commitCount,
|
|
288
|
+
branchOperations,
|
|
289
|
+
conflictResolutionRate,
|
|
290
|
+
avgConflictResolutionMs,
|
|
291
|
+
staleBranchPulls,
|
|
292
|
+
gitCommandTimeline: this.events.slice(-50),
|
|
293
|
+
conflictHistory: this.conflictRecords,
|
|
294
|
+
suggestions,
|
|
295
|
+
bestPractices,
|
|
296
|
+
preventionScore,
|
|
297
|
+
efficiencyScore,
|
|
298
|
+
riskIndicators,
|
|
299
|
+
velocityMetrics,
|
|
300
|
+
conflictResolutionStrategy,
|
|
301
|
+
prMetrics,
|
|
302
|
+
repoContext: this.repoContext,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
reset(_sessionId) {
|
|
306
|
+
this.events = [];
|
|
307
|
+
this.conflictRecords = [];
|
|
308
|
+
this.pendingConflictTimestamp = null;
|
|
309
|
+
this.pendingConflictCommand = '';
|
|
310
|
+
this.pendingConflictFiles = [];
|
|
311
|
+
this.lastSyncTimestamp = null;
|
|
312
|
+
this.pullsSinceLastConflict = 0;
|
|
313
|
+
this.consecutiveFailedPushes = 0;
|
|
314
|
+
this.statusChecksSinceLastAction = 0;
|
|
315
|
+
this.firstEditTimestamp = null;
|
|
316
|
+
this.firstSyncTimestamp = null;
|
|
317
|
+
this.commitsSinceLastSync = 0;
|
|
318
|
+
this.syncIntervalCommitCounts = [];
|
|
319
|
+
this.pushRejections = 0;
|
|
320
|
+
this.forceAfterReject = 0;
|
|
321
|
+
this.lastPushRejectedTimestamp = null;
|
|
322
|
+
this.conflictedFiles.clear();
|
|
323
|
+
this.editedFiles.clear();
|
|
324
|
+
this.hasUsedWorktree = false;
|
|
325
|
+
this.hasUsedForceWithLease = false;
|
|
326
|
+
this.totalToolCalls = 0;
|
|
327
|
+
this.sessionStartTimestamp = null;
|
|
328
|
+
this.commitTimestamps = [];
|
|
329
|
+
this.worktreeCommands = 0;
|
|
330
|
+
this.oursCount = 0;
|
|
331
|
+
this.theirsCount = 0;
|
|
332
|
+
this.cherryPickCount = 0;
|
|
333
|
+
this.lastBuildOrTestTimestamp = null;
|
|
334
|
+
this.lastPushTimestamp = null;
|
|
335
|
+
this.buildBeforePush = null;
|
|
336
|
+
this.commitsAheadOfMain = null;
|
|
337
|
+
this.commitsBehindMain = null;
|
|
338
|
+
this.quickConflictResolutions = 0;
|
|
339
|
+
this.prEvents = [];
|
|
340
|
+
this.firstCommitTimestamp = null;
|
|
341
|
+
this.repoContext = { repoName: null, branch: null, remoteName: null, defaultBranch: null };
|
|
342
|
+
}
|
|
343
|
+
// -------------------------------------------------------------------------
|
|
344
|
+
// Internals
|
|
345
|
+
// -------------------------------------------------------------------------
|
|
346
|
+
classifyGitCommand(command, record) {
|
|
347
|
+
const base = {
|
|
348
|
+
timestamp: record.timestamp,
|
|
349
|
+
command,
|
|
350
|
+
success: record.success,
|
|
351
|
+
durationMs: record.durationMs,
|
|
352
|
+
};
|
|
353
|
+
const output = record.error ?? '';
|
|
354
|
+
const hasConflict = MERGE_CONFLICT_INDICATORS.some((re) => re.test(output));
|
|
355
|
+
const hasRebaseConflict = REBASE_CONFLICT_RE.test(output);
|
|
356
|
+
const hasRejection = REJECT_INDICATORS.some((re) => re.test(output));
|
|
357
|
+
if (hasConflict && !hasRebaseConflict)
|
|
358
|
+
return { ...base, type: 'merge_conflict' };
|
|
359
|
+
if (hasRebaseConflict)
|
|
360
|
+
return { ...base, type: 'rebase_conflict' };
|
|
361
|
+
if (MERGE_ABORT_RE.test(command))
|
|
362
|
+
return { ...base, type: 'merge_abort' };
|
|
363
|
+
if (REBASE_ABORT_RE.test(command))
|
|
364
|
+
return { ...base, type: 'rebase_abort' };
|
|
365
|
+
if (CHERRY_PICK_ABORT_RE.test(command))
|
|
366
|
+
return { ...base, type: 'cherry_pick_abort' };
|
|
367
|
+
if (GIT_PUSH_FORCE_LEASE_RE.test(command))
|
|
368
|
+
return { ...base, type: 'force_push_lease' };
|
|
369
|
+
if (GIT_PUSH_FORCE_RE.test(command))
|
|
370
|
+
return { ...base, type: 'force_push' };
|
|
371
|
+
if (GIT_RESET_HARD_RE.test(command))
|
|
372
|
+
return { ...base, type: 'reset_hard' };
|
|
373
|
+
if (GIT_CHECKOUT_DASH_RE.test(command) || GIT_RESTORE_RE.test(command))
|
|
374
|
+
return { ...base, type: 'discard_changes' };
|
|
375
|
+
if (GIT_WORKTREE_RE.test(command))
|
|
376
|
+
return { ...base, type: 'worktree' };
|
|
377
|
+
if (GIT_PULL_RE.test(command))
|
|
378
|
+
return { ...base, type: 'pull' };
|
|
379
|
+
if (GIT_FETCH_RE.test(command))
|
|
380
|
+
return { ...base, type: 'fetch' };
|
|
381
|
+
if (GIT_PUSH_RE.test(command) && hasRejection)
|
|
382
|
+
return { ...base, type: 'push_rejected' };
|
|
383
|
+
if (GIT_PUSH_RE.test(command))
|
|
384
|
+
return { ...base, type: 'push' };
|
|
385
|
+
if (GIT_REBASE_RE.test(command))
|
|
386
|
+
return { ...base, type: 'rebase' };
|
|
387
|
+
if (GIT_MERGE_RE.test(command))
|
|
388
|
+
return { ...base, type: 'merge' };
|
|
389
|
+
if (GIT_STASH_RE.test(command))
|
|
390
|
+
return { ...base, type: 'stash' };
|
|
391
|
+
if (GIT_BRANCH_RE.test(command))
|
|
392
|
+
return { ...base, type: 'branch' };
|
|
393
|
+
if (GIT_COMMIT_RE.test(command))
|
|
394
|
+
return { ...base, type: 'commit' };
|
|
395
|
+
if (GIT_STATUS_RE.test(command))
|
|
396
|
+
return { ...base, type: 'status' };
|
|
397
|
+
if (GIT_DIFF_RE.test(command))
|
|
398
|
+
return { ...base, type: 'diff' };
|
|
399
|
+
if (GIT_LOG_RE.test(command))
|
|
400
|
+
return { ...base, type: 'log' };
|
|
401
|
+
return { ...base, type: 'other_git' };
|
|
402
|
+
}
|
|
403
|
+
processEvent(event, command, record) {
|
|
404
|
+
// Track conflict resolution strategies regardless of event type
|
|
405
|
+
if (GIT_CHECKOUT_OURS_RE.test(command))
|
|
406
|
+
this.oursCount++;
|
|
407
|
+
if (GIT_CHECKOUT_THEIRS_RE.test(command))
|
|
408
|
+
this.theirsCount++;
|
|
409
|
+
if (GIT_CHERRY_PICK_RE.test(command))
|
|
410
|
+
this.cherryPickCount++;
|
|
411
|
+
switch (event.type) {
|
|
412
|
+
case 'merge_conflict':
|
|
413
|
+
case 'rebase_conflict': {
|
|
414
|
+
this.pendingConflictTimestamp = event.timestamp;
|
|
415
|
+
this.pendingConflictCommand = command;
|
|
416
|
+
const output = record.error ?? '';
|
|
417
|
+
const files = [];
|
|
418
|
+
let match;
|
|
419
|
+
CONFLICT_FILE_RE.lastIndex = 0;
|
|
420
|
+
while ((match = CONFLICT_FILE_RE.exec(output)) !== null) {
|
|
421
|
+
files.push(match[1].trim());
|
|
422
|
+
this.conflictedFiles.add(match[1].trim());
|
|
423
|
+
}
|
|
424
|
+
this.pendingConflictFiles = files;
|
|
425
|
+
this.pullsSinceLastConflict = 0;
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
case 'merge_abort':
|
|
429
|
+
case 'rebase_abort':
|
|
430
|
+
case 'cherry_pick_abort':
|
|
431
|
+
if (this.pendingConflictTimestamp !== null) {
|
|
432
|
+
this.conflictRecords.push({
|
|
433
|
+
timestamp: this.pendingConflictTimestamp,
|
|
434
|
+
resolution: 'aborted',
|
|
435
|
+
resolutionTimeMs: event.timestamp - this.pendingConflictTimestamp,
|
|
436
|
+
command: this.pendingConflictCommand,
|
|
437
|
+
files: this.pendingConflictFiles,
|
|
438
|
+
});
|
|
439
|
+
this.pendingConflictTimestamp = null;
|
|
440
|
+
this.pendingConflictCommand = '';
|
|
441
|
+
this.pendingConflictFiles = [];
|
|
442
|
+
}
|
|
443
|
+
break;
|
|
444
|
+
case 'commit': {
|
|
445
|
+
// git commit --amend fixes a prior commit, not a merge conflict.
|
|
446
|
+
// Clear the pending conflict on amend so the *next* normal commit
|
|
447
|
+
// doesn't see a stale pendingConflictTimestamp (potentially hours old).
|
|
448
|
+
if (command.includes('--amend')) {
|
|
449
|
+
this.pendingConflictTimestamp = null;
|
|
450
|
+
this.pendingConflictCommand = '';
|
|
451
|
+
this.pendingConflictFiles = [];
|
|
452
|
+
}
|
|
453
|
+
if (this.pendingConflictTimestamp !== null && !command.includes('--amend')) {
|
|
454
|
+
const resolutionMs = event.timestamp - this.pendingConflictTimestamp;
|
|
455
|
+
this.conflictRecords.push({
|
|
456
|
+
timestamp: this.pendingConflictTimestamp,
|
|
457
|
+
resolution: 'resolved',
|
|
458
|
+
resolutionTimeMs: resolutionMs,
|
|
459
|
+
command: this.pendingConflictCommand,
|
|
460
|
+
files: this.pendingConflictFiles,
|
|
461
|
+
});
|
|
462
|
+
// Under 30s resolution with multiple conflicted files is suspiciously fast
|
|
463
|
+
if (resolutionMs < 30_000 && this.pendingConflictFiles.length > 1) {
|
|
464
|
+
this.quickConflictResolutions++;
|
|
465
|
+
}
|
|
466
|
+
this.pendingConflictTimestamp = null;
|
|
467
|
+
this.pendingConflictCommand = '';
|
|
468
|
+
this.pendingConflictFiles = [];
|
|
469
|
+
}
|
|
470
|
+
this.commitTimestamps.push(event.timestamp);
|
|
471
|
+
if (this.firstCommitTimestamp === null) {
|
|
472
|
+
this.firstCommitTimestamp = event.timestamp;
|
|
473
|
+
}
|
|
474
|
+
this.commitsSinceLastSync++;
|
|
475
|
+
this.statusChecksSinceLastAction = 0;
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
case 'pull':
|
|
479
|
+
case 'fetch':
|
|
480
|
+
case 'rebase':
|
|
481
|
+
if (this.firstSyncTimestamp === null) {
|
|
482
|
+
this.firstSyncTimestamp = event.timestamp;
|
|
483
|
+
}
|
|
484
|
+
this.lastSyncTimestamp = event.timestamp;
|
|
485
|
+
if (this.commitsSinceLastSync > 0) {
|
|
486
|
+
this.syncIntervalCommitCounts.push(this.commitsSinceLastSync);
|
|
487
|
+
}
|
|
488
|
+
this.commitsSinceLastSync = 0;
|
|
489
|
+
this.pullsSinceLastConflict++;
|
|
490
|
+
this.statusChecksSinceLastAction = 0;
|
|
491
|
+
break;
|
|
492
|
+
case 'push': {
|
|
493
|
+
// buildBeforePush is only meaningful if the build/test happened AFTER the
|
|
494
|
+
// most recent commit — a stale test from session start with many commits
|
|
495
|
+
// in between doesn't protect the pushed code.
|
|
496
|
+
const lastCommitTs = this.commitTimestamps.length > 0
|
|
497
|
+
? this.commitTimestamps[this.commitTimestamps.length - 1]
|
|
498
|
+
: null;
|
|
499
|
+
this.lastPushTimestamp = event.timestamp;
|
|
500
|
+
this.buildBeforePush =
|
|
501
|
+
this.lastBuildOrTestTimestamp !== null &&
|
|
502
|
+
(lastCommitTs === null || this.lastBuildOrTestTimestamp > lastCommitTs);
|
|
503
|
+
this.consecutiveFailedPushes = 0;
|
|
504
|
+
this.statusChecksSinceLastAction = 0;
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
case 'push_rejected':
|
|
508
|
+
this.pushRejections++;
|
|
509
|
+
this.consecutiveFailedPushes++;
|
|
510
|
+
this.lastPushRejectedTimestamp = event.timestamp;
|
|
511
|
+
this.statusChecksSinceLastAction = 0;
|
|
512
|
+
break;
|
|
513
|
+
case 'force_push':
|
|
514
|
+
if (this.lastPushRejectedTimestamp !== null &&
|
|
515
|
+
event.timestamp - this.lastPushRejectedTimestamp < 300_000) {
|
|
516
|
+
this.forceAfterReject++;
|
|
517
|
+
}
|
|
518
|
+
this.lastPushTimestamp = event.timestamp;
|
|
519
|
+
{
|
|
520
|
+
const lastCt = this.commitTimestamps.length > 0
|
|
521
|
+
? this.commitTimestamps[this.commitTimestamps.length - 1]
|
|
522
|
+
: null;
|
|
523
|
+
this.buildBeforePush =
|
|
524
|
+
this.lastBuildOrTestTimestamp !== null &&
|
|
525
|
+
(lastCt === null || this.lastBuildOrTestTimestamp > lastCt);
|
|
526
|
+
}
|
|
527
|
+
this.consecutiveFailedPushes = 0;
|
|
528
|
+
this.statusChecksSinceLastAction = 0;
|
|
529
|
+
break;
|
|
530
|
+
case 'force_push_lease':
|
|
531
|
+
this.hasUsedForceWithLease = true;
|
|
532
|
+
this.lastPushTimestamp = event.timestamp;
|
|
533
|
+
{
|
|
534
|
+
const lastCt = this.commitTimestamps.length > 0
|
|
535
|
+
? this.commitTimestamps[this.commitTimestamps.length - 1]
|
|
536
|
+
: null;
|
|
537
|
+
this.buildBeforePush =
|
|
538
|
+
this.lastBuildOrTestTimestamp !== null &&
|
|
539
|
+
(lastCt === null || this.lastBuildOrTestTimestamp > lastCt);
|
|
540
|
+
}
|
|
541
|
+
this.consecutiveFailedPushes = 0;
|
|
542
|
+
this.statusChecksSinceLastAction = 0;
|
|
543
|
+
break;
|
|
544
|
+
case 'worktree':
|
|
545
|
+
this.hasUsedWorktree = true;
|
|
546
|
+
this.worktreeCommands++;
|
|
547
|
+
break;
|
|
548
|
+
case 'status':
|
|
549
|
+
this.statusChecksSinceLastAction++;
|
|
550
|
+
break;
|
|
551
|
+
default:
|
|
552
|
+
this.statusChecksSinceLastAction = 0;
|
|
553
|
+
break;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
countStaleBranchPulls() {
|
|
557
|
+
let staleCount = 0;
|
|
558
|
+
for (let i = 0; i < this.events.length - 1; i++) {
|
|
559
|
+
if (this.events[i].type === 'pull') {
|
|
560
|
+
const next = this.events[i + 1];
|
|
561
|
+
if (next.type === 'merge_conflict' || next.type === 'rebase_conflict') {
|
|
562
|
+
staleCount++;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return staleCount;
|
|
567
|
+
}
|
|
568
|
+
computeRiskIndicators() {
|
|
569
|
+
// Did we sync before our first edit?
|
|
570
|
+
let syncedBeforeEditing = null;
|
|
571
|
+
if (this.firstEditTimestamp !== null) {
|
|
572
|
+
syncedBeforeEditing =
|
|
573
|
+
this.firstSyncTimestamp !== null && this.firstSyncTimestamp < this.firstEditTimestamp;
|
|
574
|
+
}
|
|
575
|
+
const now = Date.now();
|
|
576
|
+
const timeSinceLastSyncMs = this.lastSyncTimestamp !== null ? now - this.lastSyncTimestamp : null;
|
|
577
|
+
// Hot files: files that conflicted AND were subsequently edited
|
|
578
|
+
const hotFiles = [...this.conflictedFiles].filter((f) => this.editedFiles.has(f));
|
|
579
|
+
const avgCommitsBetweenSyncs = this.syncIntervalCommitCounts.length > 0
|
|
580
|
+
? this.syncIntervalCommitCounts.reduce((a, b) => a + b, 0) /
|
|
581
|
+
this.syncIntervalCommitCounts.length
|
|
582
|
+
: null;
|
|
583
|
+
const sessionDurationMs = this.sessionStartTimestamp !== null ? now - this.sessionStartTimestamp : null;
|
|
584
|
+
return {
|
|
585
|
+
syncedBeforeEditing,
|
|
586
|
+
timeSinceLastSyncMs,
|
|
587
|
+
commitsSinceLastSync: this.commitsSinceLastSync,
|
|
588
|
+
pushRejections: this.pushRejections,
|
|
589
|
+
forceAfterReject: this.forceAfterReject,
|
|
590
|
+
hotFiles,
|
|
591
|
+
usesWorktrees: this.hasUsedWorktree,
|
|
592
|
+
usesForceWithLease: this.hasUsedForceWithLease,
|
|
593
|
+
avgCommitsBetweenSyncs,
|
|
594
|
+
commitsAheadOfMain: this.commitsAheadOfMain,
|
|
595
|
+
commitsBehindMain: this.commitsBehindMain,
|
|
596
|
+
sessionDurationMs,
|
|
597
|
+
quickConflictResolutions: this.quickConflictResolutions,
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
evaluateBestPractices(risk, stats) {
|
|
601
|
+
const practices = [];
|
|
602
|
+
// 1. Sync before editing
|
|
603
|
+
if (risk.syncedBeforeEditing === null) {
|
|
604
|
+
practices.push({
|
|
605
|
+
id: 'sync_before_edit',
|
|
606
|
+
label: 'Sync before editing',
|
|
607
|
+
status: 'unknown',
|
|
608
|
+
detail: 'No edits detected yet.',
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
else if (risk.syncedBeforeEditing) {
|
|
612
|
+
practices.push({
|
|
613
|
+
id: 'sync_before_edit',
|
|
614
|
+
label: 'Sync before editing',
|
|
615
|
+
status: 'pass',
|
|
616
|
+
detail: 'Pulled/fetched before first file edit — branch was up to date.',
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
practices.push({
|
|
621
|
+
id: 'sync_before_edit',
|
|
622
|
+
label: 'Sync before editing',
|
|
623
|
+
status: 'fail',
|
|
624
|
+
detail: 'Started editing files without pulling first. Always run `git pull --rebase` or `git fetch` before beginning work to avoid conflicts.',
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
// 2. Frequent syncing (pull/fetch every ~5 commits)
|
|
628
|
+
if (stats.commitCount < 3) {
|
|
629
|
+
practices.push({
|
|
630
|
+
id: 'frequent_sync',
|
|
631
|
+
label: 'Sync frequently',
|
|
632
|
+
status: 'unknown',
|
|
633
|
+
detail: 'Not enough commits yet to evaluate sync frequency.',
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
else if (risk.commitsSinceLastSync > 8) {
|
|
637
|
+
practices.push({
|
|
638
|
+
id: 'frequent_sync',
|
|
639
|
+
label: 'Sync frequently',
|
|
640
|
+
status: 'fail',
|
|
641
|
+
detail: `${risk.commitsSinceLastSync} commits since last sync. Pull/rebase at least every 5 commits to catch divergence early. The longer you drift, the worse the conflicts.`,
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
else if (risk.commitsSinceLastSync > 5) {
|
|
645
|
+
practices.push({
|
|
646
|
+
id: 'frequent_sync',
|
|
647
|
+
label: 'Sync frequently',
|
|
648
|
+
status: 'warn',
|
|
649
|
+
detail: `${risk.commitsSinceLastSync} commits since last sync. Consider pulling soon to minimize conflict risk.`,
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
else {
|
|
653
|
+
practices.push({
|
|
654
|
+
id: 'frequent_sync',
|
|
655
|
+
label: 'Sync frequently',
|
|
656
|
+
status: stats.pullCount > 0 ? 'pass' : 'unknown',
|
|
657
|
+
detail: stats.pullCount > 0
|
|
658
|
+
? 'Good sync cadence — pulling regularly between commits.'
|
|
659
|
+
: 'No syncs detected yet.',
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
// 3. Use rebase over merge (avoids merge commits that complicate history)
|
|
663
|
+
const mergeEvents = this.events.filter((e) => e.type === 'merge');
|
|
664
|
+
const rebaseEvents = this.events.filter((e) => e.type === 'rebase');
|
|
665
|
+
if (mergeEvents.length === 0 && rebaseEvents.length === 0) {
|
|
666
|
+
practices.push({
|
|
667
|
+
id: 'prefer_rebase',
|
|
668
|
+
label: 'Prefer rebase over merge',
|
|
669
|
+
status: 'unknown',
|
|
670
|
+
detail: 'No merge or rebase operations yet.',
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
else if (mergeEvents.length > rebaseEvents.length) {
|
|
674
|
+
practices.push({
|
|
675
|
+
id: 'prefer_rebase',
|
|
676
|
+
label: 'Prefer rebase over merge',
|
|
677
|
+
status: 'warn',
|
|
678
|
+
detail: 'Using merge more than rebase. Rebasing keeps history linear and makes conflicts smaller and more localized. Use `git pull --rebase` instead of `git pull`.',
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
else {
|
|
682
|
+
practices.push({
|
|
683
|
+
id: 'prefer_rebase',
|
|
684
|
+
label: 'Prefer rebase over merge',
|
|
685
|
+
status: 'pass',
|
|
686
|
+
detail: 'Good — using rebase to stay in sync, keeping history linear.',
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
// 4. Use worktrees for parallel work
|
|
690
|
+
if (this.hasUsedWorktree) {
|
|
691
|
+
practices.push({
|
|
692
|
+
id: 'use_worktrees',
|
|
693
|
+
label: 'Use worktrees for parallel work',
|
|
694
|
+
status: 'pass',
|
|
695
|
+
detail: 'Using git worktrees — parallel tasks are isolated and cannot conflict with each other.',
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
else if (stats.mergeConflicts + stats.rebaseConflicts > 0) {
|
|
699
|
+
practices.push({
|
|
700
|
+
id: 'use_worktrees',
|
|
701
|
+
label: 'Use worktrees for parallel work',
|
|
702
|
+
status: 'fail',
|
|
703
|
+
detail: 'Conflicts detected without worktree usage. When running multiple AI sessions in parallel (or switching between tasks), use `git worktree add` to give each task its own working directory. This completely eliminates cross-session conflicts.',
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
else {
|
|
707
|
+
practices.push({
|
|
708
|
+
id: 'use_worktrees',
|
|
709
|
+
label: 'Use worktrees for parallel work',
|
|
710
|
+
status: 'unknown',
|
|
711
|
+
detail: 'No worktree usage detected. Consider worktrees if you run parallel sessions.',
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
// 5. Use --force-with-lease instead of --force
|
|
715
|
+
if (stats.forcePushes === 0) {
|
|
716
|
+
practices.push({
|
|
717
|
+
id: 'force_with_lease',
|
|
718
|
+
label: 'Use --force-with-lease',
|
|
719
|
+
status: 'unknown',
|
|
720
|
+
detail: 'No force pushes yet.',
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
else if (risk.usesForceWithLease) {
|
|
724
|
+
practices.push({
|
|
725
|
+
id: 'force_with_lease',
|
|
726
|
+
label: 'Use --force-with-lease',
|
|
727
|
+
status: 'pass',
|
|
728
|
+
detail: "Good — using --force-with-lease which refuses to overwrite remote commits you haven't seen.",
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
else {
|
|
732
|
+
practices.push({
|
|
733
|
+
id: 'force_with_lease',
|
|
734
|
+
label: 'Use --force-with-lease',
|
|
735
|
+
status: 'fail',
|
|
736
|
+
detail: 'Using bare --force instead of --force-with-lease. The --force-with-lease flag is a safety net: it refuses to push if someone else has pushed to the branch since your last fetch. Always prefer it.',
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
// 6. Don't force-push after a rejection without investigating
|
|
740
|
+
if (risk.forceAfterReject > 0) {
|
|
741
|
+
practices.push({
|
|
742
|
+
id: 'no_force_after_reject',
|
|
743
|
+
label: "Don't force-push after rejection",
|
|
744
|
+
status: 'fail',
|
|
745
|
+
detail: `Push was rejected ${risk.pushRejections} time(s) and then force-pushed ${risk.forceAfterReject} time(s). When a push is rejected, pull + rebase first to incorporate upstream changes. Force pushing after a rejection overwrites others' work.`,
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
else if (risk.pushRejections > 0) {
|
|
749
|
+
practices.push({
|
|
750
|
+
id: 'no_force_after_reject',
|
|
751
|
+
label: "Don't force-push after rejection",
|
|
752
|
+
status: 'pass',
|
|
753
|
+
detail: 'Push was rejected but correctly handled without force pushing.',
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
// 7. Keep PRs small (proxy: many commits without pushing)
|
|
757
|
+
if (risk.commitsSinceLastSync > 15) {
|
|
758
|
+
practices.push({
|
|
759
|
+
id: 'small_increments',
|
|
760
|
+
label: 'Push in small increments',
|
|
761
|
+
status: 'fail',
|
|
762
|
+
detail: `${risk.commitsSinceLastSync} local commits without pushing. Large batches create massive diffs that are more likely to conflict and harder to review. Push and open PRs early and often.`,
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
else if (stats.commitCount >= 3) {
|
|
766
|
+
practices.push({
|
|
767
|
+
id: 'small_increments',
|
|
768
|
+
label: 'Push in small increments',
|
|
769
|
+
status: 'pass',
|
|
770
|
+
detail: 'Good — committing and syncing in small batches.',
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
// 8. Avoid editing hot files
|
|
774
|
+
if (risk.hotFiles.length > 0) {
|
|
775
|
+
practices.push({
|
|
776
|
+
id: 'avoid_hot_files',
|
|
777
|
+
label: 'Avoid re-editing conflicted files',
|
|
778
|
+
status: 'warn',
|
|
779
|
+
detail: `Editing files that previously conflicted: ${risk.hotFiles.slice(0, 3).join(', ')}${risk.hotFiles.length > 3 ? ` (+${risk.hotFiles.length - 3} more)` : ''}. These are "hot" files with active upstream changes — edits here are likely to conflict again. Consider coordinating or waiting for upstream to stabilize.`,
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
// 9. Build/test before pushing
|
|
783
|
+
if (this.buildBeforePush === null && this.lastPushTimestamp === null) {
|
|
784
|
+
practices.push({
|
|
785
|
+
id: 'verify_before_push',
|
|
786
|
+
label: 'Build/test before pushing',
|
|
787
|
+
status: 'unknown',
|
|
788
|
+
detail: 'No pushes yet.',
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
else if (this.buildBeforePush === true) {
|
|
792
|
+
practices.push({
|
|
793
|
+
id: 'verify_before_push',
|
|
794
|
+
label: 'Build/test before pushing',
|
|
795
|
+
status: 'pass',
|
|
796
|
+
detail: 'Good — ran build or tests before pushing. This catches errors before they reach CI and avoids wasted review cycles.',
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
else if (this.buildBeforePush === false) {
|
|
800
|
+
practices.push({
|
|
801
|
+
id: 'verify_before_push',
|
|
802
|
+
label: 'Build/test before pushing',
|
|
803
|
+
status: 'fail',
|
|
804
|
+
detail: "Pushed without running build or tests first. Always run `npm run build && npm test` before pushing to catch issues locally — it's faster than waiting for CI.",
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
return practices;
|
|
808
|
+
}
|
|
809
|
+
computePreventionScore(practices) {
|
|
810
|
+
const scorable = practices.filter((p) => p.status !== 'unknown');
|
|
811
|
+
if (scorable.length < 2)
|
|
812
|
+
return null;
|
|
813
|
+
let points = 0;
|
|
814
|
+
let total = 0;
|
|
815
|
+
for (const p of scorable) {
|
|
816
|
+
total += 1;
|
|
817
|
+
if (p.status === 'pass')
|
|
818
|
+
points += 1;
|
|
819
|
+
else if (p.status === 'warn')
|
|
820
|
+
points += 0.5;
|
|
821
|
+
}
|
|
822
|
+
return Math.round((points / total) * 100);
|
|
823
|
+
}
|
|
824
|
+
generateSuggestions(stats) {
|
|
825
|
+
const suggestions = [];
|
|
826
|
+
// --- Proactive prevention suggestions (fire BEFORE conflicts happen) ---
|
|
827
|
+
if (stats.riskIndicators.syncedBeforeEditing === false) {
|
|
828
|
+
suggestions.push({
|
|
829
|
+
severity: 'warning',
|
|
830
|
+
category: 'no_initial_sync',
|
|
831
|
+
message: 'Started editing without syncing first. Run `git fetch && git rebase origin/main` (or your target branch) at the start of every session. This single habit prevents the majority of AI-assisted coding conflicts.',
|
|
832
|
+
evidence: 'First file edit occurred before any git pull/fetch',
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
if (stats.riskIndicators.commitsSinceLastSync > 8) {
|
|
836
|
+
suggestions.push({
|
|
837
|
+
severity: 'warning',
|
|
838
|
+
category: 'drift_risk',
|
|
839
|
+
message: `${stats.riskIndicators.commitsSinceLastSync} commits without syncing. You're accumulating drift that will compound into painful conflicts. Run \`git fetch && git rebase origin/main\` now — smaller, frequent rebases are far easier than one large one later.`,
|
|
840
|
+
evidence: `${stats.riskIndicators.commitsSinceLastSync} commits since last pull/fetch/rebase`,
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
if (stats.riskIndicators.forceAfterReject > 0) {
|
|
844
|
+
suggestions.push({
|
|
845
|
+
severity: 'critical',
|
|
846
|
+
category: 'force_after_reject',
|
|
847
|
+
message: 'Push was rejected and then force-pushed — this overwrites upstream changes. The correct response to a rejected push is: `git fetch`, then `git rebase origin/<branch>`, resolve any conflicts, then push normally. Force push is a last resort, not a first response.',
|
|
848
|
+
evidence: `${stats.riskIndicators.forceAfterReject} force push(es) within 5 min of a rejection`,
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
if (stats.riskIndicators.hotFiles.length > 0) {
|
|
852
|
+
suggestions.push({
|
|
853
|
+
severity: 'info',
|
|
854
|
+
category: 'hot_files',
|
|
855
|
+
message: `You're editing files that previously conflicted (${stats.riskIndicators.hotFiles.slice(0, 3).join(', ')}). These likely have active upstream work. Consider: (1) rebasing immediately to get latest state, (2) coordinating with whoever else is touching these files, or (3) deferring changes until upstream settles.`,
|
|
856
|
+
evidence: `${stats.riskIndicators.hotFiles.length} previously-conflicted file(s) re-edited`,
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
// --- Reactive suggestions (fire after problems occur) ---
|
|
860
|
+
if (stats.mergeConflicts + stats.rebaseConflicts >= 3) {
|
|
861
|
+
suggestions.push({
|
|
862
|
+
severity: 'critical',
|
|
863
|
+
category: 'merge_conflicts',
|
|
864
|
+
message: "Frequent merge conflicts this session. Root causes for AI assistants: (1) not pulling at session start, (2) working on stale branches too long, (3) editing files with active upstream changes. Fix: sync every 3–5 commits, use worktrees for parallel tasks, and check `git log origin/main..HEAD` to see how far you've drifted.",
|
|
865
|
+
evidence: `${stats.mergeConflicts + stats.rebaseConflicts} conflicts this session`,
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
else if (stats.mergeConflicts + stats.rebaseConflicts >= 1) {
|
|
869
|
+
suggestions.push({
|
|
870
|
+
severity: 'warning',
|
|
871
|
+
category: 'merge_conflicts',
|
|
872
|
+
message: 'Merge conflict encountered. For future prevention: `git fetch && git rebase origin/main` before starting work and after every ~5 commits. If this is a busy repo, consider shorter-lived branches and smaller PRs.',
|
|
873
|
+
evidence: `${stats.mergeConflicts + stats.rebaseConflicts} conflict(s) this session`,
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
if (stats.abortedOperations >= 2) {
|
|
877
|
+
suggestions.push({
|
|
878
|
+
severity: 'warning',
|
|
879
|
+
category: 'aborted_operations',
|
|
880
|
+
message: 'Multiple aborted merge/rebase operations suggest the branch has diverged too far. Strategy: (1) break the rebase into smaller steps with `git rebase --onto`, (2) cherry-pick only your commits onto a fresh branch, or (3) do an interactive rebase squashing first to reduce conflict surface area.',
|
|
881
|
+
evidence: `${stats.abortedOperations} aborted operations`,
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
if (stats.forcePushes >= 2) {
|
|
885
|
+
suggestions.push({
|
|
886
|
+
severity: 'critical',
|
|
887
|
+
category: 'force_push',
|
|
888
|
+
message: 'Multiple force pushes this session. Always use --force-with-lease as a safety net. If you need to rewrite history, coordinate with collaborators first and ensure your local refs are up to date with `git fetch` before force pushing.',
|
|
889
|
+
evidence: `${stats.forcePushes} force pushes this session`,
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
else if (stats.forcePushes === 1) {
|
|
893
|
+
suggestions.push({
|
|
894
|
+
severity: 'info',
|
|
895
|
+
category: 'force_push',
|
|
896
|
+
message: "Force push used. Prefer --force-with-lease for safer force pushes — it refuses to overwrite commits you haven't seen locally.",
|
|
897
|
+
evidence: '1 force push',
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
if (stats.resetHards >= 2) {
|
|
901
|
+
suggestions.push({
|
|
902
|
+
severity: 'warning',
|
|
903
|
+
category: 'reset_hard',
|
|
904
|
+
message: 'Multiple hard resets. Consider `git stash` to save work before resetting, or `git reset --mixed` to unstage without losing working tree changes.',
|
|
905
|
+
evidence: `${stats.resetHards} hard resets`,
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
if (stats.staleBranchPulls >= 2) {
|
|
909
|
+
suggestions.push({
|
|
910
|
+
severity: 'warning',
|
|
911
|
+
category: 'stale_branch',
|
|
912
|
+
message: "Pulls repeatedly cause conflicts — the branch has significantly diverged. Prevention: (1) rebase onto target branch at the START of each session, (2) use `git fetch` + `git log ..origin/main` to check divergence before pulling, (3) for long-lived branches, rebase daily even if you're not done.",
|
|
913
|
+
evidence: `${stats.staleBranchPulls} pulls that led directly to conflicts`,
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
if (stats.discardedChanges >= 3) {
|
|
917
|
+
suggestions.push({
|
|
918
|
+
severity: 'info',
|
|
919
|
+
category: 'discarded_changes',
|
|
920
|
+
message: "Frequently discarding changes. Use a scratch branch (`git checkout -b scratch/experiment`) instead — you can always delete it later, but you can't recover discarded changes.",
|
|
921
|
+
evidence: `${stats.discardedChanges} discard operations`,
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
if (stats.totalGitCommands > 10 && stats.pullCount === 0) {
|
|
925
|
+
suggestions.push({
|
|
926
|
+
severity: 'info',
|
|
927
|
+
category: 'sync_frequency',
|
|
928
|
+
message: 'No pulls detected this session despite significant git activity. On shared branches, pull at least every 15 minutes or every 5 commits — whichever comes first.',
|
|
929
|
+
evidence: `${stats.totalGitCommands} git commands, 0 pulls`,
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
if (stats.commitCount > 10 &&
|
|
933
|
+
stats.pullCount === 0 &&
|
|
934
|
+
stats.mergeConflicts + stats.rebaseConflicts === 0) {
|
|
935
|
+
suggestions.push({
|
|
936
|
+
severity: 'warning',
|
|
937
|
+
category: 'divergence_risk',
|
|
938
|
+
message: "You've made many commits without syncing. Even though there are no conflicts YET, you're accumulating divergence that makes future conflicts larger and harder to resolve. Sync now while it's easy: `git fetch && git rebase origin/main`.",
|
|
939
|
+
evidence: `${stats.commitCount} commits, 0 syncs`,
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
// --- Field guide: branch divergence from main ---
|
|
943
|
+
if (stats.riskIndicators.commitsBehindMain !== null &&
|
|
944
|
+
stats.riskIndicators.commitsBehindMain > 20) {
|
|
945
|
+
suggestions.push({
|
|
946
|
+
severity: 'warning',
|
|
947
|
+
category: 'behind_main',
|
|
948
|
+
message: `Branch is ${stats.riskIndicators.commitsBehindMain} commits behind main. The longer you wait to rebase, the more painful it gets. Run \`git fetch origin && git rebase origin/main\` before it gets worse. On an active repo, main can move 20+ commits per day.`,
|
|
949
|
+
evidence: `${stats.riskIndicators.commitsBehindMain} commits behind origin/main`,
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
else if (stats.riskIndicators.commitsBehindMain !== null &&
|
|
953
|
+
stats.riskIndicators.commitsBehindMain > 5) {
|
|
954
|
+
suggestions.push({
|
|
955
|
+
severity: 'info',
|
|
956
|
+
category: 'behind_main',
|
|
957
|
+
message: `Branch is ${stats.riskIndicators.commitsBehindMain} commits behind main. Consider rebasing soon to stay current.`,
|
|
958
|
+
evidence: `${stats.riskIndicators.commitsBehindMain} commits behind origin/main`,
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
// --- Field guide: session duration as PR size risk ---
|
|
962
|
+
if (stats.riskIndicators.sessionDurationMs !== null &&
|
|
963
|
+
stats.riskIndicators.sessionDurationMs > 2 * 3600_000 &&
|
|
964
|
+
stats.commitCount > 15) {
|
|
965
|
+
suggestions.push({
|
|
966
|
+
severity: 'info',
|
|
967
|
+
category: 'session_length',
|
|
968
|
+
message: 'Long session with many commits. The single biggest predictor of merge pain is how long a branch lives. Consider breaking this into smaller PRs that merge incrementally — a 200-line PR that ships in 30 minutes almost never conflicts.',
|
|
969
|
+
evidence: `Session running ${Math.round(stats.riskIndicators.sessionDurationMs / 3600_000)}h with ${stats.commitCount} commits`,
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
// --- Field guide: blind conflict resolution warning ---
|
|
973
|
+
if (stats.riskIndicators.quickConflictResolutions > 0) {
|
|
974
|
+
suggestions.push({
|
|
975
|
+
severity: 'warning',
|
|
976
|
+
category: 'quick_resolution',
|
|
977
|
+
message: 'Conflicts were resolved very quickly (under 30 seconds). AI-generated conflict resolutions should be reviewed line by line — they handle syntactic conflicts well but can miss semantic conflicts where two PRs modified the same logic with different intent. Run the test suite after every resolution.',
|
|
978
|
+
evidence: `${stats.riskIndicators.quickConflictResolutions} conflict(s) resolved in under 30s`,
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
// --- Field guide: suggest SessionStart hook ---
|
|
982
|
+
if (stats.riskIndicators.syncedBeforeEditing === false && stats.totalGitCommands > 3) {
|
|
983
|
+
suggestions.push({
|
|
984
|
+
severity: 'info',
|
|
985
|
+
category: 'session_hook',
|
|
986
|
+
message: 'Tip: Add a SessionStart hook to ~/.claude/settings.json that auto-runs `git fetch --all --prune` at the start of every session. Claude Code does not auto-fetch — it operates on whatever git state is on disk. The hook ensures you always start fresh without having to remember.',
|
|
987
|
+
evidence: 'No sync before first edit this session',
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
return suggestions;
|
|
991
|
+
}
|
|
992
|
+
computeScore(stats) {
|
|
993
|
+
if (stats.totalGitCommands < 3)
|
|
994
|
+
return null;
|
|
995
|
+
let score = 100;
|
|
996
|
+
const conflictPenalty = Math.min((stats.mergeConflicts + stats.rebaseConflicts) * 10, 40);
|
|
997
|
+
score -= conflictPenalty;
|
|
998
|
+
score -= Math.min(stats.abortedOperations * 15, 30);
|
|
999
|
+
score -= Math.min(stats.forcePushes * 10, 20);
|
|
1000
|
+
score -= Math.min(stats.resetHards * 5, 15);
|
|
1001
|
+
score -= Math.min(stats.discardedChanges * 3, 15);
|
|
1002
|
+
if (stats.conflictResolutionRate !== null && stats.conflictResolutionRate >= 0.8) {
|
|
1003
|
+
score += 5;
|
|
1004
|
+
}
|
|
1005
|
+
return Math.max(0, Math.min(100, score));
|
|
1006
|
+
}
|
|
1007
|
+
computeVelocityMetrics() {
|
|
1008
|
+
const sorted = [...this.commitTimestamps].sort((a, b) => a - b);
|
|
1009
|
+
let avgTimeBetweenCommitsMs = null;
|
|
1010
|
+
let longestGapMs = null;
|
|
1011
|
+
let commitBurstCount = 0;
|
|
1012
|
+
if (sorted.length >= 2) {
|
|
1013
|
+
const gaps = [];
|
|
1014
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
1015
|
+
gaps.push(sorted[i] - sorted[i - 1]);
|
|
1016
|
+
}
|
|
1017
|
+
avgTimeBetweenCommitsMs = gaps.reduce((a, b) => a + b, 0) / gaps.length;
|
|
1018
|
+
longestGapMs = gaps.reduce((max, g) => (g > max ? g : max), 0);
|
|
1019
|
+
// A "burst" is 3+ commits within 2 minutes of each other; count once per burst
|
|
1020
|
+
let consecutive = 1;
|
|
1021
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
1022
|
+
if (sorted[i] - sorted[i - 1] < 120_000) {
|
|
1023
|
+
consecutive++;
|
|
1024
|
+
if (consecutive === 3)
|
|
1025
|
+
commitBurstCount++;
|
|
1026
|
+
}
|
|
1027
|
+
else {
|
|
1028
|
+
consecutive = 1;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
return {
|
|
1033
|
+
avgTimeBetweenCommitsMs,
|
|
1034
|
+
commitBurstCount,
|
|
1035
|
+
longestGapMs,
|
|
1036
|
+
worktreeCount: this.worktreeCommands,
|
|
1037
|
+
buildBeforePush: this.buildBeforePush,
|
|
1038
|
+
// Use the push-time snapshot rather than comparing current timestamps, which go
|
|
1039
|
+
// stale when new builds run after the push.
|
|
1040
|
+
testBeforePush: this.buildBeforePush,
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
computeConflictStrategy() {
|
|
1044
|
+
const manualMergeCount = this.conflictRecords.filter((c) => c.resolution === 'resolved').length -
|
|
1045
|
+
this.oursCount -
|
|
1046
|
+
this.theirsCount;
|
|
1047
|
+
return {
|
|
1048
|
+
oursCount: this.oursCount,
|
|
1049
|
+
theirsCount: this.theirsCount,
|
|
1050
|
+
manualMergeCount: Math.max(0, manualMergeCount),
|
|
1051
|
+
cherryPickCount: this.cherryPickCount,
|
|
1052
|
+
totalResolutions: this.oursCount + this.theirsCount + Math.max(0, manualMergeCount),
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
//# sourceMappingURL=git-efficiency-tracker.js.map
|