shipwright-cli 2.4.0 → 3.1.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.
Files changed (169) hide show
  1. package/README.md +16 -11
  2. package/completions/_shipwright +248 -94
  3. package/completions/shipwright.bash +68 -19
  4. package/completions/shipwright.fish +310 -42
  5. package/config/decision-tiers.json +55 -0
  6. package/config/defaults.json +111 -0
  7. package/config/event-schema.json +218 -0
  8. package/config/policy.json +21 -18
  9. package/dashboard/coverage/coverage-summary.json +14 -0
  10. package/dashboard/public/index.html +1 -1
  11. package/dashboard/server.ts +306 -17
  12. package/dashboard/src/components/charts/bar.test.ts +79 -0
  13. package/dashboard/src/components/charts/donut.test.ts +68 -0
  14. package/dashboard/src/components/charts/pipeline-rail.test.ts +117 -0
  15. package/dashboard/src/components/charts/sparkline.test.ts +125 -0
  16. package/dashboard/src/core/api.test.ts +309 -0
  17. package/dashboard/src/core/helpers.test.ts +301 -0
  18. package/dashboard/src/core/router.test.ts +307 -0
  19. package/dashboard/src/core/router.ts +7 -0
  20. package/dashboard/src/core/sse.test.ts +144 -0
  21. package/dashboard/src/views/metrics.test.ts +186 -0
  22. package/dashboard/src/views/overview.test.ts +173 -0
  23. package/dashboard/src/views/pipelines.test.ts +183 -0
  24. package/dashboard/src/views/team.test.ts +253 -0
  25. package/dashboard/vitest.config.ts +14 -5
  26. package/docs/TIPS.md +1 -1
  27. package/docs/patterns/README.md +1 -1
  28. package/package.json +7 -9
  29. package/scripts/adapters/docker-deploy.sh +1 -1
  30. package/scripts/adapters/tmux-adapter.sh +11 -1
  31. package/scripts/adapters/wezterm-adapter.sh +1 -1
  32. package/scripts/check-version-consistency.sh +1 -1
  33. package/scripts/lib/architecture.sh +127 -0
  34. package/scripts/lib/bootstrap.sh +75 -0
  35. package/scripts/lib/compat.sh +89 -6
  36. package/scripts/lib/config.sh +91 -0
  37. package/scripts/lib/daemon-adaptive.sh +3 -3
  38. package/scripts/lib/daemon-dispatch.sh +63 -17
  39. package/scripts/lib/daemon-failure.sh +0 -0
  40. package/scripts/lib/daemon-health.sh +1 -1
  41. package/scripts/lib/daemon-patrol.sh +64 -17
  42. package/scripts/lib/daemon-poll.sh +54 -25
  43. package/scripts/lib/daemon-state.sh +125 -23
  44. package/scripts/lib/daemon-triage.sh +31 -9
  45. package/scripts/lib/decide-autonomy.sh +295 -0
  46. package/scripts/lib/decide-scoring.sh +228 -0
  47. package/scripts/lib/decide-signals.sh +462 -0
  48. package/scripts/lib/fleet-failover.sh +63 -0
  49. package/scripts/lib/helpers.sh +29 -6
  50. package/scripts/lib/pipeline-detection.sh +2 -2
  51. package/scripts/lib/pipeline-github.sh +9 -9
  52. package/scripts/lib/pipeline-intelligence.sh +105 -38
  53. package/scripts/lib/pipeline-quality-checks.sh +17 -16
  54. package/scripts/lib/pipeline-quality.sh +1 -1
  55. package/scripts/lib/pipeline-stages.sh +440 -59
  56. package/scripts/lib/pipeline-state.sh +54 -4
  57. package/scripts/lib/policy.sh +0 -0
  58. package/scripts/lib/test-helpers.sh +247 -0
  59. package/scripts/postinstall.mjs +78 -12
  60. package/scripts/signals/example-collector.sh +36 -0
  61. package/scripts/sw +17 -7
  62. package/scripts/sw-activity.sh +1 -11
  63. package/scripts/sw-adaptive.sh +109 -85
  64. package/scripts/sw-adversarial.sh +4 -14
  65. package/scripts/sw-architecture-enforcer.sh +1 -11
  66. package/scripts/sw-auth.sh +8 -17
  67. package/scripts/sw-autonomous.sh +111 -49
  68. package/scripts/sw-changelog.sh +1 -11
  69. package/scripts/sw-checkpoint.sh +144 -20
  70. package/scripts/sw-ci.sh +2 -12
  71. package/scripts/sw-cleanup.sh +13 -17
  72. package/scripts/sw-code-review.sh +16 -36
  73. package/scripts/sw-connect.sh +5 -12
  74. package/scripts/sw-context.sh +9 -26
  75. package/scripts/sw-cost.sh +17 -18
  76. package/scripts/sw-daemon.sh +76 -71
  77. package/scripts/sw-dashboard.sh +57 -17
  78. package/scripts/sw-db.sh +524 -26
  79. package/scripts/sw-decide.sh +685 -0
  80. package/scripts/sw-decompose.sh +1 -11
  81. package/scripts/sw-deps.sh +15 -25
  82. package/scripts/sw-developer-simulation.sh +1 -11
  83. package/scripts/sw-discovery.sh +138 -30
  84. package/scripts/sw-doc-fleet.sh +7 -17
  85. package/scripts/sw-docs-agent.sh +6 -16
  86. package/scripts/sw-docs.sh +4 -12
  87. package/scripts/sw-doctor.sh +134 -43
  88. package/scripts/sw-dora.sh +11 -19
  89. package/scripts/sw-durable.sh +35 -52
  90. package/scripts/sw-e2e-orchestrator.sh +11 -27
  91. package/scripts/sw-eventbus.sh +115 -115
  92. package/scripts/sw-evidence.sh +114 -30
  93. package/scripts/sw-feedback.sh +3 -13
  94. package/scripts/sw-fix.sh +2 -20
  95. package/scripts/sw-fleet-discover.sh +1 -11
  96. package/scripts/sw-fleet-viz.sh +10 -18
  97. package/scripts/sw-fleet.sh +13 -17
  98. package/scripts/sw-github-app.sh +6 -16
  99. package/scripts/sw-github-checks.sh +1 -11
  100. package/scripts/sw-github-deploy.sh +1 -11
  101. package/scripts/sw-github-graphql.sh +2 -12
  102. package/scripts/sw-guild.sh +1 -11
  103. package/scripts/sw-heartbeat.sh +49 -12
  104. package/scripts/sw-hygiene.sh +45 -43
  105. package/scripts/sw-incident.sh +48 -74
  106. package/scripts/sw-init.sh +35 -37
  107. package/scripts/sw-instrument.sh +1 -11
  108. package/scripts/sw-intelligence.sh +368 -53
  109. package/scripts/sw-jira.sh +5 -14
  110. package/scripts/sw-launchd.sh +2 -12
  111. package/scripts/sw-linear.sh +8 -17
  112. package/scripts/sw-logs.sh +4 -12
  113. package/scripts/sw-loop.sh +905 -104
  114. package/scripts/sw-memory.sh +263 -20
  115. package/scripts/sw-mission-control.sh +2 -12
  116. package/scripts/sw-model-router.sh +73 -34
  117. package/scripts/sw-otel.sh +15 -23
  118. package/scripts/sw-oversight.sh +1 -11
  119. package/scripts/sw-patrol-meta.sh +5 -11
  120. package/scripts/sw-pipeline-composer.sh +7 -17
  121. package/scripts/sw-pipeline-vitals.sh +1 -11
  122. package/scripts/sw-pipeline.sh +550 -122
  123. package/scripts/sw-pm.sh +2 -12
  124. package/scripts/sw-pr-lifecycle.sh +33 -28
  125. package/scripts/sw-predictive.sh +16 -22
  126. package/scripts/sw-prep.sh +6 -16
  127. package/scripts/sw-ps.sh +1 -11
  128. package/scripts/sw-public-dashboard.sh +2 -12
  129. package/scripts/sw-quality.sh +85 -14
  130. package/scripts/sw-reaper.sh +1 -11
  131. package/scripts/sw-recruit.sh +15 -25
  132. package/scripts/sw-regression.sh +11 -21
  133. package/scripts/sw-release-manager.sh +19 -28
  134. package/scripts/sw-release.sh +8 -16
  135. package/scripts/sw-remote.sh +1 -11
  136. package/scripts/sw-replay.sh +48 -44
  137. package/scripts/sw-retro.sh +70 -92
  138. package/scripts/sw-review-rerun.sh +1 -1
  139. package/scripts/sw-scale.sh +174 -41
  140. package/scripts/sw-security-audit.sh +12 -22
  141. package/scripts/sw-self-optimize.sh +239 -23
  142. package/scripts/sw-session.sh +5 -15
  143. package/scripts/sw-setup.sh +8 -18
  144. package/scripts/sw-standup.sh +5 -15
  145. package/scripts/sw-status.sh +32 -23
  146. package/scripts/sw-strategic.sh +129 -13
  147. package/scripts/sw-stream.sh +1 -11
  148. package/scripts/sw-swarm.sh +76 -36
  149. package/scripts/sw-team-stages.sh +10 -20
  150. package/scripts/sw-templates.sh +4 -14
  151. package/scripts/sw-testgen.sh +3 -13
  152. package/scripts/sw-tmux-pipeline.sh +1 -19
  153. package/scripts/sw-tmux-role-color.sh +0 -10
  154. package/scripts/sw-tmux-status.sh +3 -11
  155. package/scripts/sw-tmux.sh +2 -20
  156. package/scripts/sw-trace.sh +1 -19
  157. package/scripts/sw-tracker-github.sh +0 -10
  158. package/scripts/sw-tracker-jira.sh +1 -11
  159. package/scripts/sw-tracker-linear.sh +1 -11
  160. package/scripts/sw-tracker.sh +7 -24
  161. package/scripts/sw-triage.sh +29 -39
  162. package/scripts/sw-upgrade.sh +5 -23
  163. package/scripts/sw-ux.sh +1 -19
  164. package/scripts/sw-webhook.sh +18 -32
  165. package/scripts/sw-widgets.sh +3 -21
  166. package/scripts/sw-worktree.sh +11 -27
  167. package/scripts/update-homebrew-sha.sh +73 -0
  168. package/templates/pipelines/tdd.json +72 -0
  169. package/scripts/sw-pipeline.sh.mock +0 -7
@@ -0,0 +1,295 @@
1
+ # decide-autonomy.sh — Tier enforcement & rate limiting for the decision engine
2
+ # Source from sw-decide.sh. Requires helpers.sh, policy.sh.
3
+ [[ -n "${_DECIDE_AUTONOMY_LOADED:-}" ]] && return 0
4
+ _DECIDE_AUTONOMY_LOADED=1
5
+
6
+ # ─── State ────────────────────────────────────────────────────────────────────
7
+ DECISIONS_DIR="${HOME}/.shipwright/decisions"
8
+ HALT_FILE="${DECISIONS_DIR}/halt.json"
9
+ LAST_DECISION_FILE="${DECISIONS_DIR}/last-decision.json"
10
+ OUTCOMES_FILE="${DECISIONS_DIR}/outcomes.jsonl"
11
+
12
+ _ensure_decisions_dir() {
13
+ mkdir -p "$DECISIONS_DIR"
14
+ }
15
+
16
+ _daily_log_file() {
17
+ echo "${DECISIONS_DIR}/daily-log-$(date -u +%Y-%m-%d).jsonl"
18
+ }
19
+
20
+ # ─── Tier Configuration ──────────────────────────────────────────────────────
21
+
22
+ TIERS_DATA=""
23
+ CATEGORY_RULES=""
24
+ TIER_LIMITS=""
25
+
26
+ autonomy_load_tiers() {
27
+ local tiers_path="${TIERS_FILE:-}"
28
+ if [[ -z "$tiers_path" ]]; then
29
+ # Try repo-relative, then policy
30
+ local repo_dir="${_REPO_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || echo '.')}"
31
+ tiers_path="${repo_dir}/config/decision-tiers.json"
32
+ if [[ ! -f "$tiers_path" ]]; then
33
+ tiers_path=$(policy_get ".decision.tiers_file" "config/decision-tiers.json")
34
+ [[ "$tiers_path" != /* ]] && tiers_path="${repo_dir}/${tiers_path}"
35
+ fi
36
+ fi
37
+
38
+ if [[ ! -f "$tiers_path" ]]; then
39
+ return 1
40
+ fi
41
+
42
+ TIERS_FILE="$tiers_path"
43
+ TIERS_DATA=$(cat "$tiers_path")
44
+ CATEGORY_RULES=$(echo "$TIERS_DATA" | jq -c '.category_rules // {}')
45
+ TIER_LIMITS=$(echo "$TIERS_DATA" | jq -c '.limits // {}')
46
+ return 0
47
+ }
48
+
49
+ # ─── Tier Resolution ─────────────────────────────────────────────────────────
50
+
51
+ autonomy_resolve_tier() {
52
+ local category="$1"
53
+ if [[ -z "$CATEGORY_RULES" ]]; then
54
+ echo "draft"
55
+ return
56
+ fi
57
+ local tier
58
+ tier=$(echo "$CATEGORY_RULES" | jq -r --arg cat "$category" '.[$cat].tier // "draft"')
59
+ echo "${tier:-draft}"
60
+ }
61
+
62
+ autonomy_get_labels() {
63
+ local tier="$1"
64
+ if [[ -z "$TIERS_DATA" ]]; then
65
+ echo ""
66
+ return
67
+ fi
68
+ echo "$TIERS_DATA" | jq -r --arg t "$tier" '.tiers[$t].labels // [] | join(",")'
69
+ }
70
+
71
+ autonomy_get_template() {
72
+ local tier="$1"
73
+ if [[ -z "$TIERS_DATA" ]]; then
74
+ echo "standard"
75
+ return
76
+ fi
77
+ local tmpl
78
+ tmpl=$(echo "$TIERS_DATA" | jq -r --arg t "$tier" '.tiers[$t].pipeline_template // "standard"')
79
+ [[ "$tmpl" == "null" ]] && tmpl=""
80
+ echo "$tmpl"
81
+ }
82
+
83
+ # ─── Budget Checks ───────────────────────────────────────────────────────────
84
+
85
+ autonomy_check_budget() {
86
+ local tier="$1"
87
+ _ensure_decisions_dir
88
+
89
+ local daily_log
90
+ daily_log=$(_daily_log_file)
91
+
92
+ # Count today's issues created
93
+ local today_count=0
94
+ if [[ -f "$daily_log" ]]; then
95
+ today_count=$(jq -s '[.[] | select(.action == "issue_created" or .action == "draft_written")] | length' "$daily_log" 2>/dev/null || echo "0")
96
+ fi
97
+
98
+ local max_issues
99
+ max_issues=$(echo "${TIER_LIMITS:-{}}" | jq -r '.max_issues_per_day // 15')
100
+
101
+ if [[ "$today_count" -ge "$max_issues" ]]; then
102
+ return 1
103
+ fi
104
+
105
+ # Check cost budget
106
+ local max_cost
107
+ max_cost=$(echo "${TIER_LIMITS:-{}}" | jq -r '.max_cost_per_day_usd // 25')
108
+ local today_cost=0
109
+ if [[ -f "$daily_log" ]]; then
110
+ today_cost=$(jq -s '[.[] | .estimated_cost_usd // 0] | add // 0' "$daily_log" 2>/dev/null || echo "0")
111
+ fi
112
+
113
+ # Only check cost for auto tier (propose/draft are cheap)
114
+ if [[ "$tier" == "auto" ]]; then
115
+ local cost_exceeded
116
+ cost_exceeded=$(echo "$today_cost $max_cost" | awk '{print ($1 >= $2) ? "true" : "false"}')
117
+ if [[ "$cost_exceeded" == "true" ]]; then
118
+ return 1
119
+ fi
120
+ fi
121
+
122
+ return 0
123
+ }
124
+
125
+ # ─── Rate Limiting ────────────────────────────────────────────────────────────
126
+
127
+ autonomy_check_rate_limit() {
128
+ [[ ! -f "$LAST_DECISION_FILE" ]] && return 0
129
+
130
+ local last_epoch
131
+ last_epoch=$(jq -r '.epoch // 0' "$LAST_DECISION_FILE" 2>/dev/null || echo "0")
132
+ local now_e
133
+ now_e=$(now_epoch)
134
+
135
+ local cooldown
136
+ cooldown=$(echo "${TIER_LIMITS:-{}}" | jq -r '.cooldown_seconds // 300')
137
+
138
+ local elapsed=$((now_e - last_epoch))
139
+ if [[ "$elapsed" -lt "$cooldown" ]]; then
140
+ return 1
141
+ fi
142
+ return 0
143
+ }
144
+
145
+ # ─── Halt Management ─────────────────────────────────────────────────────────
146
+
147
+ autonomy_check_halt() {
148
+ [[ -f "$HALT_FILE" ]] && return 1
149
+ return 0
150
+ }
151
+
152
+ autonomy_halt() {
153
+ _ensure_decisions_dir
154
+ local reason="${1:-manual halt}"
155
+ local tmp
156
+ tmp=$(mktemp)
157
+ jq -n --arg reason "$reason" --arg ts "$(now_iso)" --argjson epoch "$(now_epoch)" \
158
+ '{halted: true, reason: $reason, halted_at: $ts, epoch: $epoch}' > "$tmp" && mv "$tmp" "$HALT_FILE"
159
+ emit_event "decision.halted" "reason=$reason"
160
+ }
161
+
162
+ autonomy_resume() {
163
+ if [[ -f "$HALT_FILE" ]]; then
164
+ rm -f "$HALT_FILE"
165
+ emit_event "decision.resumed"
166
+ fi
167
+ }
168
+
169
+ # ─── Consecutive Failure Tracking ─────────────────────────────────────────────
170
+
171
+ autonomy_check_consecutive_failures() {
172
+ _ensure_decisions_dir
173
+ local daily_log
174
+ daily_log=$(_daily_log_file)
175
+ [[ ! -f "$daily_log" ]] && return 0
176
+
177
+ local max_consecutive
178
+ max_consecutive=$(echo "${TIER_LIMITS:-{}}" | jq -r '.halt_after_consecutive_failures // 3')
179
+
180
+ # Get the last N decisions and check if all failed
181
+ local recent
182
+ recent=$(jq -s --argjson n "$max_consecutive" '. | reverse | .[:$n]' "$daily_log" 2>/dev/null || echo '[]')
183
+ local count
184
+ count=$(echo "$recent" | jq 'length' 2>/dev/null || echo "0")
185
+ [[ "$count" -lt "$max_consecutive" ]] && return 0
186
+
187
+ local all_failed
188
+ all_failed=$(echo "$recent" | jq --argjson n "$max_consecutive" \
189
+ '[.[] | select(.outcome == "failure")] | length == $n' 2>/dev/null || echo "false")
190
+
191
+ if [[ "$all_failed" == "true" ]]; then
192
+ autonomy_halt "Halted: ${max_consecutive} consecutive failures"
193
+ return 1
194
+ fi
195
+ return 0
196
+ }
197
+
198
+ # ─── Risk Ceiling ─────────────────────────────────────────────────────────────
199
+
200
+ autonomy_check_risk_ceiling() {
201
+ local category="$1"
202
+ local risk_score="$2"
203
+ [[ -z "$CATEGORY_RULES" ]] && return 0
204
+
205
+ local ceiling
206
+ ceiling=$(echo "$CATEGORY_RULES" | jq -r --arg cat "$category" '.[$cat].risk_ceiling // 100')
207
+
208
+ if [[ "$risk_score" -gt "$ceiling" ]]; then
209
+ return 1
210
+ fi
211
+ return 0
212
+ }
213
+
214
+ # ─── Decision Recording ──────────────────────────────────────────────────────
215
+
216
+ autonomy_record_decision() {
217
+ local decision_json="$1"
218
+ _ensure_decisions_dir
219
+
220
+ local daily_log
221
+ daily_log=$(_daily_log_file)
222
+
223
+ # Append to daily log (atomic via tmp + append)
224
+ echo "$decision_json" >> "$daily_log"
225
+
226
+ # Update last-decision pointer
227
+ local tmp
228
+ tmp=$(mktemp)
229
+ echo "$decision_json" | jq '. + {epoch: (now | floor)}' > "$tmp" && mv "$tmp" "$LAST_DECISION_FILE"
230
+
231
+ # Rotate old daily logs (keep 30 days)
232
+ find "$DECISIONS_DIR" -name "daily-log-*.jsonl" -mtime +30 -delete 2>/dev/null || true
233
+ }
234
+
235
+ autonomy_record_outcome() {
236
+ local decision_id="$1"
237
+ local result="$2"
238
+ local detail="${3:-}"
239
+ _ensure_decisions_dir
240
+
241
+ local outcome
242
+ outcome=$(jq -n \
243
+ --arg id "$decision_id" \
244
+ --arg result "$result" \
245
+ --arg detail "$detail" \
246
+ --arg ts "$(now_iso)" \
247
+ '{decision_id: $id, result: $result, detail: $detail, recorded_at: $ts}')
248
+
249
+ echo "$outcome" >> "$OUTCOMES_FILE"
250
+
251
+ # Update daily log entry with outcome
252
+ local daily_log
253
+ daily_log=$(_daily_log_file)
254
+ if [[ -f "$daily_log" ]]; then
255
+ local tmp
256
+ tmp=$(mktemp)
257
+ jq --arg id "$decision_id" --arg res "$result" \
258
+ 'if .id == $id then . + {outcome: $res} else . end' \
259
+ "$daily_log" > "$tmp" && mv "$tmp" "$daily_log" || rm -f "$tmp"
260
+ fi
261
+ }
262
+
263
+ # ─── Daily Summary ────────────────────────────────────────────────────────────
264
+
265
+ autonomy_daily_summary() {
266
+ _ensure_decisions_dir
267
+ local daily_log
268
+ daily_log=$(_daily_log_file)
269
+
270
+ if [[ ! -f "$daily_log" ]]; then
271
+ jq -n '{date: (now | strftime("%Y-%m-%d")), total: 0, auto: 0, propose: 0, draft: 0, budget_remaining: {issues: 15, cost_usd: 25}}'
272
+ return
273
+ fi
274
+
275
+ local max_issues max_cost
276
+ max_issues=$(echo "${TIER_LIMITS:-{}}" | jq -r '.max_issues_per_day // 15')
277
+ max_cost=$(echo "${TIER_LIMITS:-{}}" | jq -r '.max_cost_per_day_usd // 25')
278
+
279
+ jq -s --argjson mi "$max_issues" --arg mc "$max_cost" '
280
+ {
281
+ date: (now | strftime("%Y-%m-%d")),
282
+ total: length,
283
+ auto: [.[] | select(.tier == "auto")] | length,
284
+ propose: [.[] | select(.tier == "propose")] | length,
285
+ draft: [.[] | select(.tier == "draft")] | length,
286
+ successes: [.[] | select(.outcome == "success")] | length,
287
+ failures: [.[] | select(.outcome == "failure")] | length,
288
+ budget_remaining: {
289
+ issues: ($mi - length),
290
+ cost_usd: ($mc - ([.[] | .estimated_cost_usd // 0] | add // 0))
291
+ },
292
+ halted: false
293
+ }
294
+ ' "$daily_log" 2>/dev/null || echo '{}'
295
+ }
@@ -0,0 +1,228 @@
1
+ # decide-scoring.sh — Value scoring for the decision engine
2
+ # Source from sw-decide.sh. Requires helpers.sh.
3
+ [[ -n "${_DECIDE_SCORING_LOADED:-}" ]] && return 0
4
+ _DECIDE_SCORING_LOADED=1
5
+
6
+ # ─── State ────────────────────────────────────────────────────────────────────
7
+ WEIGHTS_FILE="${HOME}/.shipwright/decisions/weights.json"
8
+
9
+ # Default weights
10
+ _W_IMPACT=30
11
+ _W_URGENCY=25
12
+ _W_EFFORT=20
13
+ _W_CONFIDENCE=15
14
+ _W_RISK=10
15
+
16
+ # ─── Weight Management ───────────────────────────────────────────────────────
17
+
18
+ scoring_load_weights() {
19
+ if [[ -f "$WEIGHTS_FILE" ]]; then
20
+ _W_IMPACT=$(jq -r '.impact // 30' "$WEIGHTS_FILE" 2>/dev/null || echo "30")
21
+ _W_URGENCY=$(jq -r '.urgency // 25' "$WEIGHTS_FILE" 2>/dev/null || echo "25")
22
+ _W_EFFORT=$(jq -r '.effort // 20' "$WEIGHTS_FILE" 2>/dev/null || echo "20")
23
+ _W_CONFIDENCE=$(jq -r '.confidence // 15' "$WEIGHTS_FILE" 2>/dev/null || echo "15")
24
+ _W_RISK=$(jq -r '.risk // 10' "$WEIGHTS_FILE" 2>/dev/null || echo "10")
25
+ fi
26
+
27
+ # Also try loading from tiers config
28
+ if [[ -n "${TIERS_FILE:-}" && -f "${TIERS_FILE:-}" ]]; then
29
+ local cfg_impact
30
+ cfg_impact=$(jq -r '.scoring_weights.impact // empty' "$TIERS_FILE" 2>/dev/null || true)
31
+ if [[ -n "$cfg_impact" ]]; then
32
+ _W_IMPACT=$(echo "$cfg_impact" | awk '{printf "%.0f", $1 * 100}')
33
+ _W_URGENCY=$(jq -r '.scoring_weights.urgency' "$TIERS_FILE" | awk '{printf "%.0f", $1 * 100}')
34
+ _W_EFFORT=$(jq -r '.scoring_weights.effort' "$TIERS_FILE" | awk '{printf "%.0f", $1 * 100}')
35
+ _W_CONFIDENCE=$(jq -r '.scoring_weights.confidence' "$TIERS_FILE" | awk '{printf "%.0f", $1 * 100}')
36
+ _W_RISK=$(jq -r '.scoring_weights.risk' "$TIERS_FILE" | awk '{printf "%.0f", $1 * 100}')
37
+ fi
38
+ fi
39
+ }
40
+
41
+ scoring_save_weights() {
42
+ mkdir -p "$(dirname "$WEIGHTS_FILE")"
43
+ local tmp
44
+ tmp=$(mktemp)
45
+ jq -n \
46
+ --argjson i "$_W_IMPACT" \
47
+ --argjson u "$_W_URGENCY" \
48
+ --argjson e "$_W_EFFORT" \
49
+ --argjson c "$_W_CONFIDENCE" \
50
+ --argjson r "$_W_RISK" \
51
+ --arg ts "$(now_iso)" \
52
+ '{impact:$i, urgency:$u, effort:$e, confidence:$c, risk:$r, updated_at:$ts}' \
53
+ > "$tmp" && mv "$tmp" "$WEIGHTS_FILE"
54
+ }
55
+
56
+ # ─── Dimension Scorers ────────────────────────────────────────────────────────
57
+ # Each returns 0-100
58
+
59
+ _score_impact() {
60
+ local candidate="$1"
61
+ local signal category risk_score
62
+ signal=$(echo "$candidate" | jq -r '.signal // "unknown"')
63
+ category=$(echo "$candidate" | jq -r '.category // "unknown"')
64
+ risk_score=$(echo "$candidate" | jq -r '.risk_score // 50')
65
+
66
+ case "$signal" in
67
+ security)
68
+ local severity
69
+ severity=$(echo "$candidate" | jq -r '.evidence.severity // "medium"')
70
+ case "$severity" in
71
+ critical) echo 90 ;; high) echo 70 ;; medium) echo 50 ;; *) echo 30 ;;
72
+ esac ;;
73
+ deps)
74
+ local diff
75
+ diff=$(echo "$candidate" | jq -r '.evidence.major_versions_behind // 1')
76
+ if [[ "${diff:-1}" -ge 3 ]]; then echo 70
77
+ elif [[ "${diff:-1}" -ge 2 ]]; then echo 55
78
+ else echo 35; fi ;;
79
+ coverage) echo 45 ;;
80
+ docs) echo 30 ;;
81
+ dead_code) echo 25 ;;
82
+ performance)
83
+ local pct
84
+ pct=$(echo "$candidate" | jq -r '.evidence.regression_pct // 0')
85
+ if [[ "${pct:-0}" -ge 50 ]]; then echo 75
86
+ elif [[ "${pct:-0}" -ge 30 ]]; then echo 60
87
+ else echo 40; fi ;;
88
+ failures) echo 55 ;;
89
+ dora) echo 60 ;;
90
+ architecture) echo 50 ;;
91
+ intelligence) echo 45 ;;
92
+ *) echo 40 ;;
93
+ esac
94
+ }
95
+
96
+ _score_urgency() {
97
+ local candidate="$1"
98
+ local signal
99
+ signal=$(echo "$candidate" | jq -r '.signal // "unknown"')
100
+
101
+ case "$signal" in
102
+ security)
103
+ local severity
104
+ severity=$(echo "$candidate" | jq -r '.evidence.severity // "medium"')
105
+ case "$severity" in
106
+ critical) echo 95 ;; high) echo 75 ;; *) echo 45 ;;
107
+ esac ;;
108
+ performance) echo 60 ;;
109
+ dora) echo 55 ;;
110
+ failures) echo 65 ;;
111
+ deps) echo 35 ;;
112
+ coverage) echo 30 ;;
113
+ docs) echo 20 ;;
114
+ dead_code) echo 15 ;;
115
+ *) echo 40 ;;
116
+ esac
117
+ }
118
+
119
+ _score_effort() {
120
+ # Inverted: easy = high score, hard = low score
121
+ local candidate="$1"
122
+ local category
123
+ category=$(echo "$candidate" | jq -r '.category // "unknown"')
124
+
125
+ case "$category" in
126
+ deps_patch) echo 90 ;;
127
+ deps_minor) echo 75 ;;
128
+ doc_sync) echo 85 ;;
129
+ dead_code) echo 70 ;;
130
+ test_coverage) echo 60 ;;
131
+ security_patch) echo 65 ;;
132
+ deps_major) echo 40 ;;
133
+ security_critical) echo 45 ;;
134
+ performance_regression) echo 35 ;;
135
+ recurring_failure) echo 30 ;;
136
+ refactor_hotspot) echo 25 ;;
137
+ architecture_drift) echo 20 ;;
138
+ dora_regression) echo 30 ;;
139
+ *) echo 50 ;;
140
+ esac
141
+ }
142
+
143
+ _score_confidence() {
144
+ local candidate="$1"
145
+ local raw_conf
146
+ raw_conf=$(echo "$candidate" | jq -r '.confidence // "0.80"')
147
+ # Convert 0.0-1.0 to 0-100
148
+ echo "$raw_conf" | awk '{printf "%.0f", $1 * 100}'
149
+ }
150
+
151
+ _score_risk() {
152
+ local candidate="$1"
153
+ local risk_score
154
+ risk_score=$(echo "$candidate" | jq -r '.risk_score // 50')
155
+ echo "$risk_score"
156
+ }
157
+
158
+ # ─── Main Scorer ──────────────────────────────────────────────────────────────
159
+
160
+ score_candidate() {
161
+ local candidate="$1"
162
+
163
+ local impact urgency effort confidence risk
164
+ impact=$(_score_impact "$candidate")
165
+ urgency=$(_score_urgency "$candidate")
166
+ effort=$(_score_effort "$candidate")
167
+ confidence=$(_score_confidence "$candidate")
168
+ risk=$(_score_risk "$candidate")
169
+
170
+ # Formula: value = (impact * w1) + (urgency * w2) + (effort * w3) + (confidence * w4) - (risk * w5)
171
+ # All weights are integers summing to 100, scores are 0-100
172
+ local value
173
+ value=$(( (impact * _W_IMPACT + urgency * _W_URGENCY + effort * _W_EFFORT + confidence * _W_CONFIDENCE - risk * _W_RISK) / 100 ))
174
+
175
+ # Clamp to 0-100
176
+ [[ "$value" -lt 0 ]] && value=0
177
+ [[ "$value" -gt 100 ]] && value=100
178
+
179
+ echo "$candidate" | jq \
180
+ --argjson vs "$value" \
181
+ --argjson imp "$impact" \
182
+ --argjson urg "$urgency" \
183
+ --argjson eff "$effort" \
184
+ --argjson conf "$confidence" \
185
+ --argjson rsk "$risk" \
186
+ '. + {value_score: $vs, scores: {impact: $imp, urgency: $urg, effort: $eff, confidence: $conf, risk: $rsk}}'
187
+ }
188
+
189
+ # ─── Outcome Learning ────────────────────────────────────────────────────────
190
+ # EMA (exponential moving average) weight adjustment based on decision outcomes
191
+
192
+ scoring_update_weights() {
193
+ local outcome="$1"
194
+ local result
195
+ result=$(echo "$outcome" | jq -r '.result // "unknown"')
196
+ local alpha=20 # EMA factor (out of 100): 20% new, 80% old
197
+
198
+ # Adjust weights based on which dimension was most predictive
199
+ # Success: boost the dominant scoring dimension; Failure: dampen it
200
+ local signal
201
+ signal=$(echo "$outcome" | jq -r '.signal // "unknown"')
202
+
203
+ case "$result" in
204
+ success)
205
+ case "$signal" in
206
+ security) _W_URGENCY=$(( (_W_URGENCY * (100 - alpha) + 30 * alpha) / 100 )) ;;
207
+ deps) _W_EFFORT=$(( (_W_EFFORT * (100 - alpha) + 25 * alpha) / 100 )) ;;
208
+ performance) _W_IMPACT=$(( (_W_IMPACT * (100 - alpha) + 35 * alpha) / 100 )) ;;
209
+ failures) _W_URGENCY=$(( (_W_URGENCY * (100 - alpha) + 30 * alpha) / 100 )) ;;
210
+ *) ;; # No adjustment for generic signals
211
+ esac ;;
212
+ failure)
213
+ # On failure, slightly increase risk weight
214
+ _W_RISK=$(( (_W_RISK * (100 - alpha) + 15 * alpha) / 100 )) ;;
215
+ esac
216
+
217
+ # Normalize weights to sum to 100
218
+ local total=$(( _W_IMPACT + _W_URGENCY + _W_EFFORT + _W_CONFIDENCE + _W_RISK ))
219
+ if [[ "$total" -gt 0 && "$total" -ne 100 ]]; then
220
+ _W_IMPACT=$(( _W_IMPACT * 100 / total ))
221
+ _W_URGENCY=$(( _W_URGENCY * 100 / total ))
222
+ _W_EFFORT=$(( _W_EFFORT * 100 / total ))
223
+ _W_CONFIDENCE=$(( _W_CONFIDENCE * 100 / total ))
224
+ _W_RISK=$((100 - _W_IMPACT - _W_URGENCY - _W_EFFORT - _W_CONFIDENCE))
225
+ fi
226
+
227
+ scoring_save_weights
228
+ }