ai-consultants 2.10.1
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/.claude/commands/ai-consultants:consult.md +94 -0
- package/.claude/commands/ai-consultants:debate.md +103 -0
- package/.claude/commands/ai-consultants:help.md +30 -0
- package/CLAUDE.md +839 -0
- package/LICENSE +21 -0
- package/README.md +758 -0
- package/SKILL.md +156 -0
- package/bin/ai-consultants +96 -0
- package/docs/COST_RATES.md +172 -0
- package/docs/JSON_SCHEMA.md +305 -0
- package/docs/SETUP.md +879 -0
- package/docs/SMART_ROUTING.md +219 -0
- package/docs/cost_rates.json +143 -0
- package/package.json +47 -0
- package/references/configuration.md +141 -0
- package/references/details.md +60 -0
- package/scripts/build_context.sh +562 -0
- package/scripts/classify_question.sh +148 -0
- package/scripts/config.sh +753 -0
- package/scripts/configure.sh +1093 -0
- package/scripts/consult_all.sh +934 -0
- package/scripts/debate_round.sh +421 -0
- package/scripts/doctor.sh +642 -0
- package/scripts/followup.sh +197 -0
- package/scripts/install.sh +319 -0
- package/scripts/lib/api.sh +640 -0
- package/scripts/lib/api_query.sh +228 -0
- package/scripts/lib/cache.sh +338 -0
- package/scripts/lib/chunking.sh +880 -0
- package/scripts/lib/code_optimizer.sh +671 -0
- package/scripts/lib/common.sh +1088 -0
- package/scripts/lib/costs.sh +674 -0
- package/scripts/lib/personas.sh +372 -0
- package/scripts/lib/progress.sh +251 -0
- package/scripts/lib/reflection.sh +447 -0
- package/scripts/lib/routing.sh +566 -0
- package/scripts/lib/schema.json +165 -0
- package/scripts/lib/session.sh +327 -0
- package/scripts/lib/symbol_map.sh +445 -0
- package/scripts/lib/voting.sh +495 -0
- package/scripts/peer_review.sh +460 -0
- package/scripts/preflight_check.sh +452 -0
- package/scripts/query_aider.sh +83 -0
- package/scripts/query_amp.sh +67 -0
- package/scripts/query_claude.sh +99 -0
- package/scripts/query_codex.sh +92 -0
- package/scripts/query_cursor.sh +72 -0
- package/scripts/query_deepseek.sh +17 -0
- package/scripts/query_gemini.sh +85 -0
- package/scripts/query_glm.sh +17 -0
- package/scripts/query_grok.sh +17 -0
- package/scripts/query_kilo.sh +140 -0
- package/scripts/query_kimi.sh +67 -0
- package/scripts/query_minimax.sh +17 -0
- package/scripts/query_mistral.sh +97 -0
- package/scripts/query_ollama.sh +328 -0
- package/scripts/query_qwen3.sh +93 -0
- package/scripts/setup_wizard.sh +367 -0
- package/scripts/skillport-install.sh +196 -0
- package/scripts/synthesize.sh +332 -0
- package/templates/consultation_report.md +323 -0
- package/templates/synthesis_prompt.md +120 -0
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# voting.sh - Confidence-weighted voting system for AI Consultants
|
|
3
|
+
#
|
|
4
|
+
# Implements weighted voting algorithms to calculate consensus
|
|
5
|
+
# and recommendations based on consultant confidence scores.
|
|
6
|
+
|
|
7
|
+
# =============================================================================
|
|
8
|
+
# CONFIDENCE-WEIGHTED VOTING
|
|
9
|
+
# =============================================================================
|
|
10
|
+
|
|
11
|
+
# Calculate the weighted average of confidence scores
|
|
12
|
+
# Usage: calculate_weighted_average <json_responses_dir>
|
|
13
|
+
calculate_weighted_average() {
|
|
14
|
+
local responses_dir="$1"
|
|
15
|
+
local total_weight=0
|
|
16
|
+
local weighted_sum=0
|
|
17
|
+
|
|
18
|
+
for f in "$responses_dir"/*.json; do
|
|
19
|
+
if [[ -f "$f" && -s "$f" ]]; then
|
|
20
|
+
local confidence=$(jq -r '.confidence.score // 5' "$f" 2>/dev/null)
|
|
21
|
+
weighted_sum=$((weighted_sum + confidence))
|
|
22
|
+
total_weight=$((total_weight + 1))
|
|
23
|
+
fi
|
|
24
|
+
done
|
|
25
|
+
|
|
26
|
+
if [[ $total_weight -gt 0 ]]; then
|
|
27
|
+
echo $((weighted_sum / total_weight))
|
|
28
|
+
else
|
|
29
|
+
echo 5
|
|
30
|
+
fi
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# Strip common suffixes for basic stemming (portable, no external tools)
|
|
34
|
+
# "sessions" → "session", "caching" → "cach", "authentication" → "authenticat"
|
|
35
|
+
# Usage: stem=$(_stem_word "sessions")
|
|
36
|
+
_stem_word() {
|
|
37
|
+
local w="$1"
|
|
38
|
+
# Longest suffixes first; only strip -ing generically (not consonant+ing
|
|
39
|
+
# variants like *ting/*ring which cause "testing"→"tes", "string"→"st")
|
|
40
|
+
case "$w" in
|
|
41
|
+
*ation) echo "${w%ation}" ;;
|
|
42
|
+
*tion) echo "${w%tion}" ;;
|
|
43
|
+
*sion) echo "${w%sion}" ;;
|
|
44
|
+
*ment) echo "${w%ment}" ;;
|
|
45
|
+
*ness) echo "${w%ness}" ;;
|
|
46
|
+
*ing) echo "${w%ing}" ;;
|
|
47
|
+
*ies) echo "${w%ies}y" ;;
|
|
48
|
+
*ses) echo "${w%es}" ;;
|
|
49
|
+
*es) echo "${w%es}" ;;
|
|
50
|
+
*s) echo "${w%s}" ;;
|
|
51
|
+
*ed) echo "${w%ed}" ;;
|
|
52
|
+
*ly) echo "${w%ly}" ;;
|
|
53
|
+
*) echo "$w" ;;
|
|
54
|
+
esac
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# Extract keywords from an approach string (portable, Bash 3.2 compatible)
|
|
58
|
+
# Lowercases, removes stop words, applies basic stemming, outputs sorted unique keywords
|
|
59
|
+
# Usage: keywords=$(_extract_keywords "JWT-based authentication")
|
|
60
|
+
_extract_keywords() {
|
|
61
|
+
echo "$1" | tr -cs '[:alnum:]' '\n' | tr '[:upper:]' '[:lower:]' | \
|
|
62
|
+
while IFS= read -r word; do
|
|
63
|
+
case "$word" in
|
|
64
|
+
the|a|an|of|for|to|in|on|with|and|or|is|are|was|were|be|been|being|based|using|use|"") ;;
|
|
65
|
+
*)
|
|
66
|
+
# Apply basic stemming to improve matching
|
|
67
|
+
if [[ ${#word} -gt 3 ]]; then
|
|
68
|
+
_stem_word "$word"
|
|
69
|
+
else
|
|
70
|
+
echo "$word"
|
|
71
|
+
fi
|
|
72
|
+
;;
|
|
73
|
+
esac
|
|
74
|
+
done | sort -u | tr '\n' ' '
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# Calculate Jaccard similarity between two keyword strings (integer 0-100)
|
|
78
|
+
# Inputs are already sorted+unique from _extract_keywords, so no re-sorting needed.
|
|
79
|
+
# Uses process substitution instead of temp files to avoid O(n^2) filesystem ops.
|
|
80
|
+
# Usage: sim=$(_jaccard_similarity "jwt auth tokens" "jwt token authentication")
|
|
81
|
+
_jaccard_similarity() {
|
|
82
|
+
local set_a="$1"
|
|
83
|
+
local set_b="$2"
|
|
84
|
+
|
|
85
|
+
local intersection union_count
|
|
86
|
+
intersection=$(comm -12 <(echo "$set_a" | tr ' ' '\n' | grep -v '^$') \
|
|
87
|
+
<(echo "$set_b" | tr ' ' '\n' | grep -v '^$') | wc -l | tr -d ' ')
|
|
88
|
+
union_count=$(echo "$set_a $set_b" | tr ' ' '\n' | sort -u | grep -v '^$' | wc -l | tr -d ' ')
|
|
89
|
+
|
|
90
|
+
if [[ $union_count -gt 0 ]]; then
|
|
91
|
+
echo $((intersection * 100 / union_count))
|
|
92
|
+
else
|
|
93
|
+
echo 0
|
|
94
|
+
fi
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# Calculate consensus score based on keyword overlap between approaches
|
|
98
|
+
# Uses Jaccard similarity instead of exact string matching so that
|
|
99
|
+
# "JWT tokens", "JWT-based auth", and "JSON Web Tokens" are recognized as similar.
|
|
100
|
+
# Usage: calculate_consensus_score <json_responses_dir>
|
|
101
|
+
calculate_consensus_score() {
|
|
102
|
+
local responses_dir="$1"
|
|
103
|
+
local approaches=()
|
|
104
|
+
local keyword_sets=()
|
|
105
|
+
|
|
106
|
+
# Collect all approaches and extract keywords, skipping "unknown" approaches
|
|
107
|
+
for f in "$responses_dir"/*.json; do
|
|
108
|
+
if [[ -f "$f" && -s "$f" ]]; then
|
|
109
|
+
local approach
|
|
110
|
+
approach=$(jq -r '.response.approach // "unknown"' "$f" 2>/dev/null)
|
|
111
|
+
# Skip unknown/empty approaches — they add noise without signal
|
|
112
|
+
local approach_lower
|
|
113
|
+
approach_lower=$(echo "$approach" | tr '[:upper:]' '[:lower:]')
|
|
114
|
+
case "$approach_lower" in
|
|
115
|
+
unknown|""|n/a|none|"not available") continue ;;
|
|
116
|
+
esac
|
|
117
|
+
approaches+=("$approach")
|
|
118
|
+
keyword_sets+=("$(_extract_keywords "$approach")")
|
|
119
|
+
fi
|
|
120
|
+
done
|
|
121
|
+
|
|
122
|
+
local total=${#approaches[@]}
|
|
123
|
+
if [[ $total -le 1 ]]; then
|
|
124
|
+
[[ $total -eq 1 ]] && echo 100 || echo 0
|
|
125
|
+
return
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
# Calculate pairwise Jaccard similarity
|
|
129
|
+
local similar_pairs=0
|
|
130
|
+
local total_pairs=0
|
|
131
|
+
local threshold=20 # Jaccard >= 0.20 means approaches are similar (lowered from 30 to account for verbose names)
|
|
132
|
+
|
|
133
|
+
local i j
|
|
134
|
+
for ((i=0; i<total; i++)); do
|
|
135
|
+
for ((j=i+1; j<total; j++)); do
|
|
136
|
+
total_pairs=$((total_pairs + 1))
|
|
137
|
+
local sim
|
|
138
|
+
sim=$(_jaccard_similarity "${keyword_sets[$i]}" "${keyword_sets[$j]}")
|
|
139
|
+
if [[ $sim -ge $threshold ]]; then
|
|
140
|
+
similar_pairs=$((similar_pairs + 1))
|
|
141
|
+
fi
|
|
142
|
+
done
|
|
143
|
+
done
|
|
144
|
+
|
|
145
|
+
# Score = percentage of pairs that are similar
|
|
146
|
+
if [[ $total_pairs -gt 0 ]]; then
|
|
147
|
+
echo $((similar_pairs * 100 / total_pairs))
|
|
148
|
+
else
|
|
149
|
+
echo 100
|
|
150
|
+
fi
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
# Determine the consensus level from a score
|
|
154
|
+
# Usage: get_consensus_level <score>
|
|
155
|
+
get_consensus_level() {
|
|
156
|
+
local score=$1
|
|
157
|
+
|
|
158
|
+
if [[ $score -ge 100 ]]; then
|
|
159
|
+
echo "unanimous"
|
|
160
|
+
elif [[ $score -ge 75 ]]; then
|
|
161
|
+
echo "high"
|
|
162
|
+
elif [[ $score -ge 50 ]]; then
|
|
163
|
+
echo "medium"
|
|
164
|
+
elif [[ $score -ge 25 ]]; then
|
|
165
|
+
echo "low"
|
|
166
|
+
else
|
|
167
|
+
echo "none"
|
|
168
|
+
fi
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
# Calculate the confidence-weighted recommendation
|
|
172
|
+
# Usage: calculate_weighted_recommendation <json_responses_dir>
|
|
173
|
+
# Output: JSON with the recommendation
|
|
174
|
+
calculate_weighted_recommendation() {
|
|
175
|
+
local responses_dir="$1"
|
|
176
|
+
|
|
177
|
+
# Clear any previous map state
|
|
178
|
+
map_clear "APPROACH_WEIGHTS"
|
|
179
|
+
map_clear "APPROACH_SUPPORTERS"
|
|
180
|
+
|
|
181
|
+
# Collect weights for each approach
|
|
182
|
+
for f in "$responses_dir"/*.json; do
|
|
183
|
+
if [[ -f "$f" && -s "$f" ]]; then
|
|
184
|
+
local consultant approach confidence current_weight current_supporters
|
|
185
|
+
consultant=$(jq -r '.consultant // "unknown"' "$f" 2>/dev/null)
|
|
186
|
+
approach=$(jq -r '.response.approach // "unknown"' "$f" 2>/dev/null)
|
|
187
|
+
confidence=$(jq -r '.confidence.score // 5' "$f" 2>/dev/null)
|
|
188
|
+
|
|
189
|
+
# Add weight
|
|
190
|
+
current_weight=$(map_get "APPROACH_WEIGHTS" "$approach")
|
|
191
|
+
map_set "APPROACH_WEIGHTS" "$approach" "$((${current_weight:-0} + confidence))"
|
|
192
|
+
|
|
193
|
+
# Add supporter
|
|
194
|
+
current_supporters=$(map_get "APPROACH_SUPPORTERS" "$approach")
|
|
195
|
+
if [[ -z "$current_supporters" ]]; then
|
|
196
|
+
map_set "APPROACH_SUPPORTERS" "$approach" "$consultant"
|
|
197
|
+
else
|
|
198
|
+
map_set "APPROACH_SUPPORTERS" "$approach" "${current_supporters},$consultant"
|
|
199
|
+
fi
|
|
200
|
+
fi
|
|
201
|
+
done
|
|
202
|
+
|
|
203
|
+
# Find the approach with the highest weight using map_keys
|
|
204
|
+
local best_approach=""
|
|
205
|
+
local best_weight=0
|
|
206
|
+
local best_supporters=""
|
|
207
|
+
local approach weight
|
|
208
|
+
|
|
209
|
+
for approach in $(map_keys "APPROACH_WEIGHTS"); do
|
|
210
|
+
weight=$(map_get "APPROACH_WEIGHTS" "$approach")
|
|
211
|
+
weight="${weight:-0}"
|
|
212
|
+
if [[ $weight -gt $best_weight ]]; then
|
|
213
|
+
best_weight=$weight
|
|
214
|
+
best_approach="$approach"
|
|
215
|
+
best_supporters=$(map_get "APPROACH_SUPPORTERS" "$approach")
|
|
216
|
+
fi
|
|
217
|
+
done
|
|
218
|
+
|
|
219
|
+
# Output JSON
|
|
220
|
+
jq -n \
|
|
221
|
+
--arg approach "$best_approach" \
|
|
222
|
+
--argjson weight "$best_weight" \
|
|
223
|
+
--arg supporters "$best_supporters" \
|
|
224
|
+
'{
|
|
225
|
+
recommended_approach: $approach,
|
|
226
|
+
total_weight: $weight,
|
|
227
|
+
supporters: ($supporters | split(","))
|
|
228
|
+
}'
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
# Identify dissenters (those who do not support the winning approach)
|
|
232
|
+
# Usage: get_dissenters <json_responses_dir> <winning_approach>
|
|
233
|
+
get_dissenters() {
|
|
234
|
+
local responses_dir="$1"
|
|
235
|
+
local winning_approach="$2"
|
|
236
|
+
local dissenters=()
|
|
237
|
+
|
|
238
|
+
for f in "$responses_dir"/*.json; do
|
|
239
|
+
if [[ -f "$f" && -s "$f" ]]; then
|
|
240
|
+
local consultant=$(jq -r '.consultant // "unknown"' "$f" 2>/dev/null)
|
|
241
|
+
local approach=$(jq -r '.response.approach // "unknown"' "$f" 2>/dev/null)
|
|
242
|
+
|
|
243
|
+
if [[ "$approach" != "$winning_approach" ]]; then
|
|
244
|
+
dissenters+=("$consultant")
|
|
245
|
+
fi
|
|
246
|
+
fi
|
|
247
|
+
done
|
|
248
|
+
|
|
249
|
+
printf '%s\n' ${dissenters[@]+"${dissenters[@]}"}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
# Calculate confidence-weighted final score
|
|
253
|
+
# Usage: calculate_final_score <json_responses_dir> <winning_approach>
|
|
254
|
+
calculate_final_score() {
|
|
255
|
+
local responses_dir="$1"
|
|
256
|
+
local winning_approach="$2"
|
|
257
|
+
|
|
258
|
+
local weighted_sum=0
|
|
259
|
+
local total_confidence=0
|
|
260
|
+
|
|
261
|
+
for f in "$responses_dir"/*.json; do
|
|
262
|
+
if [[ -f "$f" && -s "$f" ]]; then
|
|
263
|
+
local approach=$(jq -r '.response.approach // "unknown"' "$f" 2>/dev/null)
|
|
264
|
+
local confidence=$(jq -r '.confidence.score // 5' "$f" 2>/dev/null)
|
|
265
|
+
|
|
266
|
+
total_confidence=$((total_confidence + confidence))
|
|
267
|
+
|
|
268
|
+
# Full weight if supports, half if neutral, zero if dissents
|
|
269
|
+
if [[ "$approach" == "$winning_approach" ]]; then
|
|
270
|
+
weighted_sum=$((weighted_sum + confidence * 10)) # Full support
|
|
271
|
+
else
|
|
272
|
+
weighted_sum=$((weighted_sum + confidence * 2)) # Dissent but consider
|
|
273
|
+
fi
|
|
274
|
+
fi
|
|
275
|
+
done
|
|
276
|
+
|
|
277
|
+
if [[ $total_confidence -gt 0 ]]; then
|
|
278
|
+
# Normalize to 1-10 scale
|
|
279
|
+
local score=$((weighted_sum / total_confidence))
|
|
280
|
+
if [[ $score -gt 10 ]]; then
|
|
281
|
+
score=10
|
|
282
|
+
fi
|
|
283
|
+
echo $score
|
|
284
|
+
else
|
|
285
|
+
echo 5
|
|
286
|
+
fi
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
# =============================================================================
|
|
290
|
+
# MAJORITY VOTING (simple)
|
|
291
|
+
# =============================================================================
|
|
292
|
+
|
|
293
|
+
# Simple majority vote (unweighted)
|
|
294
|
+
# Usage: simple_majority_vote <json_responses_dir>
|
|
295
|
+
simple_majority_vote() {
|
|
296
|
+
local responses_dir="$1"
|
|
297
|
+
|
|
298
|
+
# Clear any previous map state
|
|
299
|
+
map_clear "MAJORITY_VOTES"
|
|
300
|
+
|
|
301
|
+
for f in "$responses_dir"/*.json; do
|
|
302
|
+
if [[ -f "$f" && -s "$f" ]]; then
|
|
303
|
+
local approach current_votes
|
|
304
|
+
approach=$(jq -r '.response.approach // "unknown"' "$f" 2>/dev/null)
|
|
305
|
+
current_votes=$(map_get "MAJORITY_VOTES" "$approach")
|
|
306
|
+
map_set "MAJORITY_VOTES" "$approach" "$((${current_votes:-0} + 1))"
|
|
307
|
+
fi
|
|
308
|
+
done
|
|
309
|
+
|
|
310
|
+
# Find approach with most votes using map_keys
|
|
311
|
+
local winner=""
|
|
312
|
+
local max_votes=0
|
|
313
|
+
local approach votes
|
|
314
|
+
|
|
315
|
+
for approach in $(map_keys "MAJORITY_VOTES"); do
|
|
316
|
+
votes=$(map_get "MAJORITY_VOTES" "$approach")
|
|
317
|
+
votes="${votes:-0}"
|
|
318
|
+
if [[ $votes -gt $max_votes ]]; then
|
|
319
|
+
max_votes=$votes
|
|
320
|
+
winner="$approach"
|
|
321
|
+
fi
|
|
322
|
+
done
|
|
323
|
+
|
|
324
|
+
echo "$winner"
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
# =============================================================================
|
|
328
|
+
# CONFIDENCE INTERVALS (v2.2)
|
|
329
|
+
# =============================================================================
|
|
330
|
+
|
|
331
|
+
# Calculate standard deviation of confidence scores
|
|
332
|
+
# Usage: calculate_confidence_stddev <json_responses_dir>
|
|
333
|
+
# Returns: Standard deviation value (integer, scaled by 10 for precision)
|
|
334
|
+
calculate_confidence_stddev() {
|
|
335
|
+
local responses_dir="$1"
|
|
336
|
+
local scores=()
|
|
337
|
+
local sum=0
|
|
338
|
+
local count=0
|
|
339
|
+
|
|
340
|
+
# Collect all confidence scores
|
|
341
|
+
for f in "$responses_dir"/*.json; do
|
|
342
|
+
if [[ -f "$f" && -s "$f" ]]; then
|
|
343
|
+
local score=$(jq -r '.confidence.score // 5' "$f" 2>/dev/null)
|
|
344
|
+
scores+=("$score")
|
|
345
|
+
sum=$((sum + score))
|
|
346
|
+
count=$((count + 1))
|
|
347
|
+
fi
|
|
348
|
+
done
|
|
349
|
+
|
|
350
|
+
if [[ $count -lt 2 ]]; then
|
|
351
|
+
echo 0
|
|
352
|
+
return
|
|
353
|
+
fi
|
|
354
|
+
|
|
355
|
+
# Calculate mean (scaled by 10 for precision)
|
|
356
|
+
local mean_scaled=$((sum * 10 / count))
|
|
357
|
+
|
|
358
|
+
# Calculate variance
|
|
359
|
+
local variance_sum=0
|
|
360
|
+
for score in "${scores[@]}"; do
|
|
361
|
+
local diff=$((score * 10 - mean_scaled))
|
|
362
|
+
variance_sum=$((variance_sum + diff * diff))
|
|
363
|
+
done
|
|
364
|
+
local variance=$((variance_sum / count))
|
|
365
|
+
|
|
366
|
+
# Approximate square root using Newton's method (integer math)
|
|
367
|
+
# For small numbers, this gives reasonable results
|
|
368
|
+
if [[ $variance -eq 0 ]]; then
|
|
369
|
+
echo 0
|
|
370
|
+
return
|
|
371
|
+
fi
|
|
372
|
+
|
|
373
|
+
local sqrt=$variance
|
|
374
|
+
local prev=0
|
|
375
|
+
for _ in {1..10}; do
|
|
376
|
+
prev=$sqrt
|
|
377
|
+
# Guard against division by zero
|
|
378
|
+
[[ $sqrt -eq 0 ]] && sqrt=1
|
|
379
|
+
sqrt=$(( (sqrt + variance / sqrt) / 2 ))
|
|
380
|
+
[[ $sqrt -eq $prev ]] && break
|
|
381
|
+
done
|
|
382
|
+
|
|
383
|
+
# Return scaled by 10 (so 15 means 1.5)
|
|
384
|
+
echo $sqrt
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
# Calculate confidence interval
|
|
388
|
+
# Usage: calculate_confidence_interval <json_responses_dir>
|
|
389
|
+
# Output: JSON with mean, stddev, low, high bounds
|
|
390
|
+
calculate_confidence_interval() {
|
|
391
|
+
local responses_dir="$1"
|
|
392
|
+
|
|
393
|
+
local mean=$(calculate_weighted_average "$responses_dir")
|
|
394
|
+
local stddev_scaled=$(calculate_confidence_stddev "$responses_dir")
|
|
395
|
+
|
|
396
|
+
# Convert scaled stddev back to decimal format for display
|
|
397
|
+
local stddev_int=$((stddev_scaled / 10))
|
|
398
|
+
local stddev_dec=$((stddev_scaled % 10))
|
|
399
|
+
|
|
400
|
+
# Calculate bounds (mean ± stddev)
|
|
401
|
+
local low=$((mean * 10 - stddev_scaled))
|
|
402
|
+
local high=$((mean * 10 + stddev_scaled))
|
|
403
|
+
|
|
404
|
+
# Clamp to valid range [1, 10]
|
|
405
|
+
[[ $low -lt 10 ]] && low=10
|
|
406
|
+
[[ $high -gt 100 ]] && high=100
|
|
407
|
+
|
|
408
|
+
local low_int=$((low / 10))
|
|
409
|
+
local low_dec=$((low % 10))
|
|
410
|
+
local high_int=$((high / 10))
|
|
411
|
+
local high_dec=$((high % 10))
|
|
412
|
+
|
|
413
|
+
jq -n \
|
|
414
|
+
--argjson mean "$mean" \
|
|
415
|
+
--arg stddev "${stddev_int}.${stddev_dec}" \
|
|
416
|
+
--arg low "${low_int}.${low_dec}" \
|
|
417
|
+
--arg high "${high_int}.${high_dec}" \
|
|
418
|
+
--argjson variance_high "$([ $stddev_scaled -gt 20 ] && echo "true" || echo "false")" \
|
|
419
|
+
'{
|
|
420
|
+
mean: $mean,
|
|
421
|
+
stddev: ($stddev | tonumber),
|
|
422
|
+
interval: {
|
|
423
|
+
low: ($low | tonumber),
|
|
424
|
+
high: ($high | tonumber)
|
|
425
|
+
},
|
|
426
|
+
display: "\($mean) ± \($stddev)",
|
|
427
|
+
high_variance: $variance_high
|
|
428
|
+
}'
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
# Check if confidence scores have high variance (uncertainty indicator)
|
|
432
|
+
# Usage: has_high_confidence_variance <json_responses_dir>
|
|
433
|
+
# Returns: 0 (true) if variance is high, 1 (false) otherwise
|
|
434
|
+
has_high_confidence_variance() {
|
|
435
|
+
local responses_dir="$1"
|
|
436
|
+
local stddev_scaled=$(calculate_confidence_stddev "$responses_dir")
|
|
437
|
+
|
|
438
|
+
# Consider variance "high" if stddev > 2.0 (scaled value > 20)
|
|
439
|
+
[[ $stddev_scaled -gt 20 ]]
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
# Get confidence range as formatted string
|
|
443
|
+
# Usage: format_confidence_range <json_responses_dir>
|
|
444
|
+
# Output: "7 ± 1.5" or "7 (±1.5, high variance!)"
|
|
445
|
+
format_confidence_range() {
|
|
446
|
+
local responses_dir="$1"
|
|
447
|
+
local interval_json=$(calculate_confidence_interval "$responses_dir")
|
|
448
|
+
|
|
449
|
+
local display=$(echo "$interval_json" | jq -r '.display')
|
|
450
|
+
local high_variance=$(echo "$interval_json" | jq -r '.high_variance')
|
|
451
|
+
|
|
452
|
+
if [[ "$high_variance" == "true" ]]; then
|
|
453
|
+
echo "$display (high variance - uncertainty detected)"
|
|
454
|
+
else
|
|
455
|
+
echo "$display"
|
|
456
|
+
fi
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
# =============================================================================
|
|
460
|
+
# UTILITY
|
|
461
|
+
# =============================================================================
|
|
462
|
+
|
|
463
|
+
# Generate complete voting report
|
|
464
|
+
# Usage: generate_voting_report <json_responses_dir>
|
|
465
|
+
generate_voting_report() {
|
|
466
|
+
local responses_dir="$1"
|
|
467
|
+
|
|
468
|
+
local consensus_score=$(calculate_consensus_score "$responses_dir")
|
|
469
|
+
local consensus_level=$(get_consensus_level "$consensus_score")
|
|
470
|
+
local avg_confidence=$(calculate_weighted_average "$responses_dir")
|
|
471
|
+
local confidence_interval=$(calculate_confidence_interval "$responses_dir")
|
|
472
|
+
local recommendation=$(calculate_weighted_recommendation "$responses_dir")
|
|
473
|
+
local winning_approach=$(echo "$recommendation" | jq -r '.recommended_approach')
|
|
474
|
+
local final_score=$(calculate_final_score "$responses_dir" "$winning_approach")
|
|
475
|
+
|
|
476
|
+
jq -n \
|
|
477
|
+
--argjson consensus_score "$consensus_score" \
|
|
478
|
+
--arg consensus_level "$consensus_level" \
|
|
479
|
+
--argjson avg_confidence "$avg_confidence" \
|
|
480
|
+
--argjson confidence_interval "$confidence_interval" \
|
|
481
|
+
--argjson recommendation "$recommendation" \
|
|
482
|
+
--argjson final_score "$final_score" \
|
|
483
|
+
'{
|
|
484
|
+
voting_report: {
|
|
485
|
+
consensus: {
|
|
486
|
+
score: $consensus_score,
|
|
487
|
+
level: $consensus_level
|
|
488
|
+
},
|
|
489
|
+
average_confidence: $avg_confidence,
|
|
490
|
+
confidence_interval: $confidence_interval,
|
|
491
|
+
recommendation: $recommendation,
|
|
492
|
+
final_weighted_score: $final_score
|
|
493
|
+
}
|
|
494
|
+
}'
|
|
495
|
+
}
|