shipwright-cli 3.0.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.
- package/README.md +3 -3
- package/completions/_shipwright +247 -93
- package/completions/shipwright.bash +69 -15
- package/completions/shipwright.fish +309 -41
- package/config/decision-tiers.json +55 -0
- package/config/event-schema.json +142 -5
- package/config/policy.json +8 -0
- package/package.json +3 -3
- package/scripts/lib/architecture.sh +2 -1
- package/scripts/lib/bootstrap.sh +0 -0
- package/scripts/lib/config.sh +0 -0
- package/scripts/lib/daemon-adaptive.sh +0 -0
- package/scripts/lib/daemon-dispatch.sh +24 -1
- package/scripts/lib/daemon-failure.sh +0 -0
- package/scripts/lib/daemon-health.sh +0 -0
- package/scripts/lib/daemon-patrol.sh +40 -5
- package/scripts/lib/daemon-poll.sh +17 -0
- package/scripts/lib/daemon-state.sh +10 -0
- package/scripts/lib/daemon-triage.sh +1 -1
- package/scripts/lib/decide-autonomy.sh +295 -0
- package/scripts/lib/decide-scoring.sh +228 -0
- package/scripts/lib/decide-signals.sh +462 -0
- package/scripts/lib/fleet-failover.sh +0 -0
- package/scripts/lib/helpers.sh +16 -17
- package/scripts/lib/pipeline-detection.sh +0 -0
- package/scripts/lib/pipeline-github.sh +0 -0
- package/scripts/lib/pipeline-intelligence.sh +20 -3
- package/scripts/lib/pipeline-quality-checks.sh +3 -2
- package/scripts/lib/pipeline-quality.sh +0 -0
- package/scripts/lib/pipeline-stages.sh +199 -32
- package/scripts/lib/pipeline-state.sh +14 -0
- package/scripts/lib/policy.sh +0 -0
- package/scripts/lib/test-helpers.sh +0 -0
- package/scripts/postinstall.mjs +75 -1
- package/scripts/signals/example-collector.sh +36 -0
- package/scripts/sw +8 -4
- package/scripts/sw-activity.sh +1 -1
- package/scripts/sw-adaptive.sh +1 -1
- package/scripts/sw-adversarial.sh +1 -1
- package/scripts/sw-architecture-enforcer.sh +1 -1
- package/scripts/sw-auth.sh +1 -1
- package/scripts/sw-autonomous.sh +1 -1
- package/scripts/sw-changelog.sh +1 -1
- package/scripts/sw-checkpoint.sh +1 -1
- package/scripts/sw-ci.sh +1 -1
- package/scripts/sw-cleanup.sh +1 -1
- package/scripts/sw-code-review.sh +1 -1
- package/scripts/sw-connect.sh +1 -1
- package/scripts/sw-context.sh +1 -1
- package/scripts/sw-cost.sh +12 -3
- package/scripts/sw-daemon.sh +2 -2
- package/scripts/sw-dashboard.sh +1 -1
- package/scripts/sw-db.sh +41 -34
- package/scripts/sw-decide.sh +685 -0
- package/scripts/sw-decompose.sh +1 -1
- package/scripts/sw-deps.sh +1 -1
- package/scripts/sw-developer-simulation.sh +1 -1
- package/scripts/sw-discovery.sh +27 -1
- package/scripts/sw-doc-fleet.sh +1 -1
- package/scripts/sw-docs-agent.sh +1 -1
- package/scripts/sw-docs.sh +1 -1
- package/scripts/sw-doctor.sh +1 -1
- package/scripts/sw-dora.sh +1 -1
- package/scripts/sw-durable.sh +1 -1
- package/scripts/sw-e2e-orchestrator.sh +1 -1
- package/scripts/sw-eventbus.sh +1 -1
- package/scripts/sw-evidence.sh +1 -1
- package/scripts/sw-feedback.sh +1 -1
- package/scripts/sw-fix.sh +1 -1
- package/scripts/sw-fleet-discover.sh +1 -1
- package/scripts/sw-fleet-viz.sh +1 -1
- package/scripts/sw-fleet.sh +1 -1
- package/scripts/sw-github-app.sh +1 -1
- package/scripts/sw-github-checks.sh +1 -1
- package/scripts/sw-github-deploy.sh +1 -1
- package/scripts/sw-github-graphql.sh +1 -1
- package/scripts/sw-guild.sh +1 -1
- package/scripts/sw-heartbeat.sh +1 -1
- package/scripts/sw-hygiene.sh +1 -1
- package/scripts/sw-incident.sh +1 -1
- package/scripts/sw-init.sh +1 -1
- package/scripts/sw-instrument.sh +1 -1
- package/scripts/sw-intelligence.sh +9 -5
- package/scripts/sw-jira.sh +1 -1
- package/scripts/sw-launchd.sh +1 -1
- package/scripts/sw-linear.sh +1 -1
- package/scripts/sw-logs.sh +1 -1
- package/scripts/sw-loop.sh +267 -17
- package/scripts/sw-memory.sh +22 -5
- package/scripts/sw-mission-control.sh +1 -1
- package/scripts/sw-model-router.sh +1 -1
- package/scripts/sw-otel.sh +5 -3
- package/scripts/sw-oversight.sh +1 -1
- package/scripts/sw-pipeline-composer.sh +1 -1
- package/scripts/sw-pipeline-vitals.sh +1 -1
- package/scripts/sw-pipeline.sh +73 -1
- package/scripts/sw-pm.sh +1 -1
- package/scripts/sw-pr-lifecycle.sh +7 -4
- package/scripts/sw-predictive.sh +1 -1
- package/scripts/sw-prep.sh +1 -1
- package/scripts/sw-ps.sh +1 -1
- package/scripts/sw-public-dashboard.sh +1 -1
- package/scripts/sw-quality.sh +9 -5
- package/scripts/sw-reaper.sh +1 -1
- package/scripts/sw-regression.sh +1 -1
- package/scripts/sw-release-manager.sh +1 -1
- package/scripts/sw-release.sh +1 -1
- package/scripts/sw-remote.sh +1 -1
- package/scripts/sw-replay.sh +1 -1
- package/scripts/sw-retro.sh +1 -1
- package/scripts/sw-review-rerun.sh +1 -1
- package/scripts/sw-scale.sh +66 -10
- package/scripts/sw-security-audit.sh +1 -1
- package/scripts/sw-self-optimize.sh +1 -1
- package/scripts/sw-session.sh +3 -3
- package/scripts/sw-setup.sh +1 -1
- package/scripts/sw-standup.sh +1 -1
- package/scripts/sw-status.sh +1 -1
- package/scripts/sw-strategic.sh +1 -1
- package/scripts/sw-stream.sh +1 -1
- package/scripts/sw-swarm.sh +1 -1
- package/scripts/sw-team-stages.sh +1 -1
- package/scripts/sw-templates.sh +1 -1
- package/scripts/sw-testgen.sh +1 -1
- package/scripts/sw-tmux-pipeline.sh +1 -1
- package/scripts/sw-tmux.sh +1 -1
- package/scripts/sw-trace.sh +1 -1
- package/scripts/sw-tracker.sh +1 -1
- package/scripts/sw-triage.sh +6 -6
- package/scripts/sw-upgrade.sh +1 -1
- package/scripts/sw-ux.sh +1 -1
- package/scripts/sw-webhook.sh +1 -1
- package/scripts/sw-widgets.sh +1 -1
- package/scripts/sw-worktree.sh +1 -1
- package/scripts/update-homebrew-sha.sh +21 -15
|
@@ -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
|
+
}
|