completion-kit 0.28.46 → 0.29.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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/completion_kit/application.js +52 -23
  3. data/app/assets/stylesheets/completion_kit/application.css +44 -14
  4. data/app/controllers/completion_kit/api/v1/metric_versions_controller.rb +2 -0
  5. data/app/controllers/completion_kit/api/v1/metrics_controller.rb +1 -1
  6. data/app/controllers/completion_kit/api/v1/runs_controller.rb +4 -1
  7. data/app/controllers/completion_kit/metrics_controller.rb +48 -46
  8. data/app/controllers/completion_kit/runs_controller.rb +4 -1
  9. data/app/helpers/completion_kit/application_helper.rb +99 -45
  10. data/app/jobs/completion_kit/check_review_job.rb +27 -6
  11. data/app/models/completion_kit/metric.rb +103 -28
  12. data/app/models/completion_kit/metric_version.rb +16 -1
  13. data/app/models/completion_kit/review.rb +2 -0
  14. data/app/models/completion_kit/run.rb +19 -8
  15. data/app/services/completion_kit/checks/contains.rb +2 -8
  16. data/app/services/completion_kit/checks/equals.rb +2 -7
  17. data/app/services/completion_kit/checks/expected_resolver.rb +17 -3
  18. data/app/services/completion_kit/checks/flag.rb +13 -0
  19. data/app/services/completion_kit/checks/json_path_equals.rb +2 -15
  20. data/app/services/completion_kit/checks/labels.rb +104 -0
  21. data/app/services/completion_kit/checks/length_bounds.rb +4 -4
  22. data/app/services/completion_kit/checks/list_overlap.rb +96 -0
  23. data/app/services/completion_kit/checks/not_contains.rb +2 -8
  24. data/app/services/completion_kit/checks/numeric_bounds.rb +22 -0
  25. data/app/services/completion_kit/checks/numeric_equals.rb +37 -0
  26. data/app/services/completion_kit/checks/numeric_value.rb +45 -0
  27. data/app/services/completion_kit/checks/regex.rb +2 -2
  28. data/app/services/completion_kit/checks/registry.rb +107 -4
  29. data/app/services/completion_kit/checks/result.rb +5 -1
  30. data/app/services/completion_kit/checks/target_resolver.rb +43 -8
  31. data/app/services/completion_kit/checks/textual.rb +19 -0
  32. data/app/services/completion_kit/mcp_tools/metrics.rb +21 -7
  33. data/app/services/completion_kit/promptfoo_importer.rb +4 -4
  34. data/app/services/completion_kit/run_comparison.rb +6 -0
  35. data/app/views/completion_kit/api_reference/_body.html.erb +7 -3
  36. data/app/views/completion_kit/metrics/_check_spec.html.erb +5 -11
  37. data/app/views/completion_kit/metrics/_form.html.erb +62 -47
  38. data/app/views/completion_kit/responses/show.html.erb +1 -1
  39. data/app/views/completion_kit/runs/_compare_score.html.erb +1 -1
  40. data/app/views/completion_kit/runs/_form.html.erb +9 -3
  41. data/app/views/completion_kit/runs/_response_row.html.erb +3 -2
  42. data/app/views/completion_kit/runs/_row.html.erb +10 -8
  43. data/app/views/completion_kit/runs/_run_config.html.erb +1 -1
  44. data/app/views/completion_kit/runs/_status_panel.html.erb +2 -2
  45. data/app/views/completion_kit/runs/compare.html.erb +10 -6
  46. data/app/views/completion_kit/shared/_branded_select.html.erb +1 -1
  47. data/config/locales/en.yml +2 -0
  48. data/db/migrate/20260903000001_add_check_score_to_completion_kit_reviews.rb +5 -0
  49. data/lib/completion_kit/version.rb +1 -1
  50. metadata +9 -1
@@ -17,10 +17,11 @@
17
17
  <% pub = local_assigns[:published_metric_version] %>
18
18
  <% draft_instr_changed = pub && pub.instruction.to_s != edit_draft.instruction.to_s %>
19
19
  <% draft_rubric_changed = pub && pub.rubric_bands != edit_draft.rubric_bands %>
20
+ <% draft_subject = edit_draft.check? ? "check" : [("instruction" if draft_instr_changed), ("rubric" if draft_rubric_changed)].compact.join(" and ").presence || "version" %>
20
21
  <div class="ck-suggestion-banner" role="status">
21
22
  <div class="ck-suggestion-banner__body">
22
23
  <p class="ck-kicker">Draft pending</p>
23
- <p class="ck-meta-copy">The form below shows your unpublished draft. Publish to replace the live<%= " instruction" if draft_instr_changed %><%= " and" if draft_instr_changed && draft_rubric_changed %><%= " rubric" if draft_rubric_changed %> for future runs, or keep editing.</p>
24
+ <p class="ck-meta-copy">The form below shows your unpublished draft. Publish to replace the live <%= draft_subject %> for future runs, or keep editing.</p>
24
25
  </div>
25
26
  <div class="ck-suggestion-banner__actions">
26
27
  <%= button_to "Discard draft", dismiss_suggestion_metric_path(metric, draft_id: edit_draft.id, back_to: "edit"),
@@ -164,99 +165,113 @@
164
165
 
165
166
  <% if show_check %>
166
167
  <% check = metric.check_config || {} %>
167
- <div class="ck-field ck-field--spacious" data-ck-metric-editor="check" <%= "hidden" unless metric.check? %>>
168
+ <% kind = check["check_kind"] || CompletionKit::Checks::Registry.kinds.first %>
169
+ <div class="ck-field ck-field--spacious" data-ck-metric-editor="check" data-ck-check-form="<%= ck_check_form_data.to_json %>" <%= "hidden" unless metric.check? %>>
168
170
  <p class="ck-section-title">Check</p>
169
171
  <p class="ck-hint">A pass or fail rule with no AI and no cost. You only fill in the fields the chosen rule needs.</p>
170
172
 
171
173
  <div class="ck-field">
172
- <p class="ck-label" id="metric_check_kind_label">Rule</p>
174
+ <p class="ck-label" id="metric_check_kind_label"><%= ck_check_field_label("check_kind") %></p>
173
175
  <%= render "completion_kit/shared/branded_select",
174
176
  name: "metric[check_config][check_kind]",
175
- options: CompletionKit::Checks::Registry.kinds.map { |kind| [kind, ck_check_kind_label(kind)] },
176
- selected: check["check_kind"] || CompletionKit::Checks::Registry.kinds.first,
177
- labelledby: "metric_check_kind_label" %>
177
+ options: CompletionKit::Checks::Registry.kinds.map { |option| [option, ck_check_kind_label(option)] },
178
+ selected: kind,
179
+ labelledby: "metric_check_kind_label",
180
+ invalid: ck_check_field_invalid?(metric, "check_kind") %>
178
181
  </div>
179
182
 
180
183
  <div class="ck-field">
181
- <p class="ck-label" id="metric_check_target_label">What to check</p>
184
+ <p class="ck-label" id="metric_check_target_label"><%= ck_check_field_label("target") %></p>
182
185
  <%= render "completion_kit/shared/branded_select",
183
186
  name: "metric[check_config][target]",
184
187
  options: CompletionKit::Checks::TargetResolver::TARGETS.map { |target| [target, ck_check_target_label(target)] },
185
188
  selected: check["target"] || CompletionKit::Checks::TargetResolver::TARGETS.first,
186
- labelledby: "metric_check_target_label" %>
189
+ labelledby: "metric_check_target_label",
190
+ invalid: ck_check_field_invalid?(metric, "target") %>
187
191
  </div>
188
192
 
189
193
  <div class="ck-field" data-ck-check-field="target_path">
190
- <label class="ck-label" for="metric_check_target_path">Path into the JSON</label>
191
- <p class="ck-hint">Where to look inside the response, like data.items.0.name.</p>
192
- <input type="text" name="metric[check_config][target_path]" id="metric_check_target_path" class="ck-input" value="<%= check["target_path"] %>">
194
+ <label class="ck-label" for="metric_check_target_path"><%= ck_check_field_label("target_path") %></label>
195
+ <p class="ck-hint">Where to look inside the response, like data.items.0.name. Use a number to pick an item out of a list.</p>
196
+ <input type="text" name="metric[check_config][target_path]" id="metric_check_target_path" class="ck-input" value="<%= check["target_path"] %>" <%= tag.attributes(ck_check_field_aria(metric, "target_path")) %>>
193
197
  </div>
194
198
 
195
199
  <div class="ck-field" data-ck-check-field="compare_to">
196
- <p class="ck-label">Compare against</p>
200
+ <p class="ck-label"><%= ck_check_field_label("compare_to") %></p>
197
201
  <p class="ck-hint">Check every output against one fixed value, or against each row's own expected value (its answer key).</p>
198
202
  <label class="ck-radio">
199
- <input type="radio" name="metric[check_config][compare_to]" value="constant" <%= "checked" unless check["compare_to"] == "expected" %>>
200
- <span>A value I type</span>
203
+ <input type="radio" name="metric[check_config][compare_to]" value="constant" <%= "checked" unless check["compare_to"] == "expected" %> <%= tag.attributes(ck_check_field_aria(metric, "compare_to")) %>>
204
+ <span><%= ck_check_field_value("compare_to", "constant") %></span>
201
205
  </label>
202
206
  <label class="ck-radio">
203
- <input type="radio" name="metric[check_config][compare_to]" value="expected" <%= "checked" if check["compare_to"] == "expected" %>>
204
- <span>Each row's expected value</span>
207
+ <input type="radio" name="metric[check_config][compare_to]" value="expected" <%= "checked" if check["compare_to"] == "expected" %> <%= tag.attributes(ck_check_field_aria(metric, "compare_to")) %>>
208
+ <span><%= ck_check_field_value("compare_to", "expected") %></span>
205
209
  </label>
206
210
  </div>
207
211
 
208
212
  <div class="ck-field" data-ck-check-field="value">
209
- <label class="ck-label" for="metric_check_value">Text to look for</label>
210
- <input type="text" name="metric[check_config][value]" id="metric_check_value" class="ck-input" value="<%= check["value"] %>">
213
+ <label class="ck-label" for="metric_check_value"><span data-ck-check-label="value"><%= ck_check_field_label("value", kind) %></span></label>
214
+ <input type="text" name="metric[check_config][value]" id="metric_check_value" class="ck-input" value="<%= ck_check_input_value(check["value"]) %>" <%= tag.attributes(ck_check_field_aria(metric, "value")) %>>
211
215
  </div>
212
216
 
213
217
  <div class="ck-field" data-ck-check-field="pattern">
214
- <label class="ck-label" for="metric_check_pattern">Pattern</label>
218
+ <label class="ck-label" for="metric_check_pattern"><%= ck_check_field_label("pattern") %></label>
215
219
  <p class="ck-hint">A regular expression to match against.</p>
216
- <input type="text" name="metric[check_config][pattern]" id="metric_check_pattern" class="ck-input" value="<%= check["pattern"] %>">
220
+ <input type="text" name="metric[check_config][pattern]" id="metric_check_pattern" class="ck-input" value="<%= check["pattern"] %>" <%= tag.attributes(ck_check_field_aria(metric, "pattern")) %>>
217
221
  </div>
218
222
 
219
223
  <div class="ck-field" data-ck-check-field="json_path">
220
- <label class="ck-label" for="metric_check_json_path">JSON path</label>
224
+ <label class="ck-label" for="metric_check_json_path"><%= ck_check_field_label("json_path") %></label>
221
225
  <p class="ck-hint">The path to the value inside the response, like data.status.</p>
222
- <input type="text" name="metric[check_config][json_path]" id="metric_check_json_path" class="ck-input" value="<%= check["json_path"] %>">
226
+ <input type="text" name="metric[check_config][json_path]" id="metric_check_json_path" class="ck-input" value="<%= check["json_path"] %>" <%= tag.attributes(ck_check_field_aria(metric, "json_path")) %>>
223
227
  </div>
224
228
 
225
229
  <div class="ck-field" data-ck-check-field="expected">
226
- <label class="ck-label" for="metric_check_expected">Expected value</label>
227
- <p class="ck-hint">The value it should equal.</p>
228
- <input type="text" name="metric[check_config][expected]" id="metric_check_expected" class="ck-input" value="<%= check["expected"] %>">
230
+ <label class="ck-label" for="metric_check_expected"><%= ck_check_field_label("expected") %></label>
231
+ <p class="ck-hint">The value it should equal. A number, true or false matches that JSON value. Put it in quotes, like "200", to match it as text.</p>
232
+ <input type="text" name="metric[check_config][expected]" id="metric_check_expected" class="ck-input" value="<%= ck_check_expected_input_value(check["expected"]) %>" <%= tag.attributes(ck_check_field_aria(metric, "expected")) %>>
229
233
  </div>
230
234
 
231
235
  <div class="ck-field" data-ck-check-field="expected_path">
232
- <label class="ck-label" for="metric_check_expected_path">Field in the answer key</label>
236
+ <label class="ck-label" for="metric_check_expected_path"><%= ck_check_field_label("expected_path") %></label>
233
237
  <p class="ck-hint">Optional. If the expected value is JSON, name the field to read from it, like data.answer. Leave blank to compare against the whole value.</p>
234
- <input type="text" name="metric[check_config][expected_path]" id="metric_check_expected_path" class="ck-input" value="<%= check["expected_path"] %>">
238
+ <input type="text" name="metric[check_config][expected_path]" id="metric_check_expected_path" class="ck-input" value="<%= check["expected_path"] %>" <%= tag.attributes(ck_check_field_aria(metric, "expected_path")) %>>
239
+ </div>
240
+
241
+ <div class="ck-field" data-ck-check-field="score_by">
242
+ <p class="ck-label" id="metric_check_score_by_label"><%= ck_check_field_label("score_by") %></p>
243
+ <p class="ck-hint"><%= ck_check_field_hint("score_by") %></p>
244
+ <%= render "completion_kit/shared/branded_select",
245
+ name: "metric[check_config][score_by]",
246
+ options: ck_check_score_by_options,
247
+ selected: check["score_by"] || CompletionKit::Checks::ListOverlap::DEFAULT_MEASURE,
248
+ labelledby: "metric_check_score_by_label",
249
+ invalid: ck_check_field_invalid?(metric, "score_by") %>
235
250
  </div>
236
251
 
237
252
  <div class="ck-field-row">
238
- <div class="ck-field" data-ck-check-field="min">
239
- <label class="ck-label" for="metric_check_min">Shortest allowed</label>
240
- <input type="number" name="metric[check_config][min]" id="metric_check_min" class="ck-input" value="<%= check["min"] %>">
241
- </div>
242
- <div class="ck-field" data-ck-check-field="max">
243
- <label class="ck-label" for="metric_check_max">Longest allowed</label>
244
- <input type="number" name="metric[check_config][max]" id="metric_check_max" class="ck-input" value="<%= check["max"] %>">
245
- </div>
253
+ <% %w[min max].each do |bound| %>
254
+ <div class="ck-field" data-ck-check-field="<%= bound %>">
255
+ <label class="ck-label" for="metric_check_<%= bound %>"><span data-ck-check-label="<%= bound %>"><%= ck_check_field_label(bound, kind) %></span></label>
256
+ <p class="ck-hint" data-ck-check-hint="<%= bound %>"><%= ck_check_field_hint(bound, kind) %></p>
257
+ <input type="number" step="any" name="metric[check_config][<%= bound %>]" id="metric_check_<%= bound %>" class="ck-input" value="<%= check[bound] %>" <%= tag.attributes(ck_check_field_aria(metric, bound)) %>>
258
+ </div>
259
+ <% end %>
260
+ </div>
261
+
262
+ <div class="ck-field" data-ck-check-field="tolerance">
263
+ <label class="ck-label" for="metric_check_tolerance"><%= ck_check_field_label("tolerance") %></label>
264
+ <p class="ck-hint">How far a number can be out and still count as correct. Write a plain number for a fixed amount, or add a percent sign, like 2%, for a share of the expected number. Leave blank to require an exact match.</p>
265
+ <input type="text" inputmode="decimal" name="metric[check_config][tolerance]" id="metric_check_tolerance" class="ck-input" value="<%= check["tolerance"] %>" <%= tag.attributes(ck_check_field_aria(metric, "tolerance")) %>>
246
266
  </div>
247
267
 
248
- <label class="ck-checkbox" data-ck-check-field="case_sensitive">
249
- <input type="checkbox" name="metric[check_config][case_sensitive]" value="true"<%= " checked" if check["case_sensitive"] %>>
250
- <span>Case sensitive</span>
251
- </label>
252
- <label class="ck-checkbox" data-ck-check-field="multiline">
253
- <input type="checkbox" name="metric[check_config][multiline]" value="true"<%= " checked" if check["multiline"] %>>
254
- <span>Multiline</span>
255
- </label>
256
- <label class="ck-checkbox" data-ck-check-field="trim">
257
- <input type="checkbox" name="metric[check_config][trim]" value="true"<%= " checked" if check["trim"] %>>
258
- <span>Trim whitespace</span>
259
- </label>
268
+ <% CompletionKit::Checks::Registry::FLAG_KEYS.each do |flag| %>
269
+ <label class="ck-checkbox" data-ck-check-field="<%= flag %>">
270
+ <input type="hidden" name="metric[check_config][<%= flag %>]" value="false">
271
+ <input type="checkbox" name="metric[check_config][<%= flag %>]" value="true"<%= " checked" if CompletionKit::Checks::Flag.on?(check, flag) %>>
272
+ <span><%= ck_check_field_label(flag) %></span>
273
+ </label>
274
+ <% end %>
260
275
  </div>
261
276
  <% end %>
262
277
 
@@ -125,7 +125,7 @@
125
125
  <svg viewBox="0 0 24 24" width="16" height="16" stroke-width="1.75" class="ck-star <%= i < review.ai_score.to_i ? "ck-star--filled" : "ck-star--empty" %>"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
126
126
  <% end %>
127
127
  <% elsif review.check? && !review.passed.nil? %>
128
- <%= ck_check_badge(review.passed) %>
128
+ <%= ck_check_badge(review.passed, review.check_score) %>
129
129
  <% elsif review.status == "failed" %>
130
130
  <span class="<%= ck_badge_classes(:failed) %>" title="<%= review.error_message.presence || (review.check? ? "Check failed" : "Judge failed") %>">Failed</span>
131
131
  <% else %>
@@ -2,7 +2,7 @@
2
2
  <% if has_value && response %>
3
3
  <%= link_to run_response_path(run, response), class: "ck-cmp-score__link", title: "#{side}: open this response (#{run.name})" do %>
4
4
  <% if kind == "check" %>
5
- <%= ck_check_badge(passed) %>
5
+ <%= ck_check_badge(passed, local_assigns[:check_score]) %>
6
6
  <% else %>
7
7
  <span class="<%= ck_badge_classes(ck_score_kind(score.to_f)) %>"><%= score %></span>
8
8
  <% end %>
@@ -161,7 +161,7 @@
161
161
  <%= hidden_field_tag "run[metric_ids][]", "", id: nil %>
162
162
  <div class="ck-metric-checkboxes" data-metric-checkboxes role="group" aria-labelledby="run_metrics_label">
163
163
  <% @all_metrics.each do |metric| %>
164
- <label class="ck-checkbox-label" data-metric-tags="<%= metric.tag_names.join(",") %>" data-compare-expected="<%= metric.check? && metric.check_config.to_h["compare_to"] == "expected" ? "1" : "0" %>">
164
+ <label class="ck-checkbox-label" data-metric-tags="<%= metric.tag_names.join(",") %>" data-needs-judge="<%= metric.llm_judge? ? "1" : "0" %>" data-compare-expected="<%= metric.check? && metric.check_config.to_h["compare_to"] == "expected" ? "1" : "0" %>">
165
165
  <%= check_box_tag "run[metric_ids][]", metric.id, run.selected_metric_ids.include?(metric.id), class: "ck-checkbox", id: "run_metric_#{metric.id}" %>
166
166
  <span class="ck-checkbox-label__box" aria-hidden="true"></span>
167
167
  <span class="ck-checkbox-label__body">
@@ -209,9 +209,15 @@ function updateRunForm() {
209
209
  if (judgeHint) judgeHint.textContent = '';
210
210
  if (metricsHint) metricsHint.textContent = '';
211
211
 
212
+ var judgedMetrics = Array.prototype.filter.call(metrics, function(cb) {
213
+ var lbl = cb.closest('[data-needs-judge]');
214
+ return !!lbl && lbl.dataset.needsJudge === '1';
215
+ });
212
216
  var judgeUnpaired = judgeListed && !!judge && metrics.length === 0;
213
- var metricsUnpaired = judgeListed && !judge && metrics.length > 0;
214
- if (judgeListed && judge && metrics.length > 0) {
217
+ var metricsUnpaired = judgeListed && !judge && judgedMetrics.length > 0;
218
+ if (judgeListed && judge && metrics.length > 0 && judgedMetrics.length === 0) {
219
+ if (judgeHint) judgeHint.textContent = 'Checks need no judge, so this run will not use it.';
220
+ } else if (judgeListed && judge && metrics.length > 0) {
215
221
  if (judgeHint) judgeHint.textContent = judgeOnly ? 'Existing outputs will be judged automatically.' : 'Responses will be generated then judged automatically.';
216
222
  } else if (judgeUnpaired) {
217
223
  if (judgeField) judgeField.className = 'ck-field ck-field--info';
@@ -21,9 +21,10 @@
21
21
  <span class="ck-metric-pip__label" aria-hidden="true"><%= r.metric_name %> <strong><%= r.ai_score %></strong></span>
22
22
  </span>
23
23
  <% else %>
24
- <span class="ck-metric-pip ck-metric-pip--<%= ck_pass_rate_kind(r.passed ? 1.0 : 0.0) %>" role="img" aria-label="<%= r.metric_name %> <%= r.passed ? "Pass" : "Fail" %>">
24
+ <% verdict = ck_check_verdict(r.passed, r.check_score) %>
25
+ <span class="ck-metric-pip ck-metric-pip--<%= ck_pass_rate_kind(r.passed ? 1.0 : 0.0) %>" role="img" aria-label="<%= r.metric_name %> <%= verdict %>">
25
26
  <span class="ck-metric-pip__bar"></span>
26
- <span class="ck-metric-pip__label" aria-hidden="true"><%= r.metric_name %> <strong><%= r.passed ? "Pass" : "Fail" %></strong></span>
27
+ <span class="ck-metric-pip__label" aria-hidden="true"><%= r.metric_name %> <strong><%= verdict %></strong></span>
27
28
  </span>
28
29
  <% end %>
29
30
  <% end %>
@@ -36,9 +36,9 @@
36
36
  <div class="ck-metric-bar ck-metric-bar--compact">
37
37
  <% metrics.each do |m| %>
38
38
  <% if m[:kind] == "check" %>
39
- <span class="ck-metric-pip ck-metric-pip--<%= ck_pass_rate_kind(m[:pass_rate]) %>" role="img" aria-label="<%= m[:name] %> <%= (m[:pass_rate] * 100).round %>% pass rate">
39
+ <span class="ck-metric-pip ck-metric-pip--<%= ck_pass_rate_kind(m[:pass_rate]) %>" role="img" aria-label="<%= ck_check_average_label(m[:name], m) %>">
40
40
  <span class="ck-metric-pip__bar"></span>
41
- <span class="ck-metric-pip__label" aria-hidden="true"><%= m[:name] %> <strong><%= (m[:pass_rate] * 100).round %>%</strong></span>
41
+ <span class="ck-metric-pip__label" aria-hidden="true"><%= m[:name] %> <strong><%= ck_check_average(m) %></strong></span>
42
42
  </span>
43
43
  <% else %>
44
44
  <span class="ck-metric-pip ck-metric-pip--<%= ck_score_kind(m[:avg]) %>" role="img" aria-label="<%= m[:name] %> average score <%= m[:avg] %>">
@@ -56,12 +56,14 @@
56
56
  <% avg = run.avg_score %>
57
57
  <% pass_rate = run.check_pass_rate %>
58
58
  <% if avg || pass_rate %>
59
- <% if avg %>
60
- <span class="<%= ck_badge_classes(ck_score_kind(avg)) %>"><%= avg %></span>
61
- <% end %>
62
- <% if pass_rate %>
63
- <span class="<%= ck_badge_classes(ck_pass_rate_kind(pass_rate)) %>"><%= (pass_rate * 100).round %>%</span>
64
- <% end %>
59
+ <div class="ck-chip-row">
60
+ <% if avg %>
61
+ <span class="<%= ck_badge_classes(ck_score_kind(avg)) %>"><%= avg %></span>
62
+ <% end %>
63
+ <% if pass_rate %>
64
+ <span class="<%= ck_badge_classes(ck_pass_rate_kind(pass_rate)) %>"><%= (pass_rate * 100).round %>%</span>
65
+ <% end %>
66
+ </div>
65
67
  <% else %>
66
68
  <span class="ck-runs-table__dim">—</span>
67
69
  <% end %>
@@ -19,7 +19,7 @@
19
19
  <div class="ck-run-config__row">
20
20
  <span class="ck-run-config__key">Judge</span>
21
21
  <span style="text-transform: none;"><%= run.judge_model %></span>
22
- <% unless run.judge_configured? %>
22
+ <% if run.llm_metrics.any? && !run.llm_judge_configured? %>
23
23
  <span class="ck-run-config__warn">provider not configured</span>
24
24
  <% end %>
25
25
  </div>
@@ -41,9 +41,9 @@
41
41
  <% run.metrics.order(:name).each do |metric| %>
42
42
  <% avg_for_metric = metric_lookup[metric.name.to_s.downcase] %>
43
43
  <% if avg_for_metric && avg_for_metric[:kind] == "check" %>
44
- <span class="ck-metric-pip ck-metric-pip--<%= ck_pass_rate_kind(avg_for_metric[:pass_rate]) %>">
44
+ <span class="ck-metric-pip ck-metric-pip--<%= ck_pass_rate_kind(avg_for_metric[:pass_rate]) %>" title="<%= ck_check_average_label(metric.name, avg_for_metric) %>">
45
45
  <span class="ck-metric-pip__bar"></span>
46
- <span class="ck-metric-pip__label"><%= metric.name %> <strong><%= (avg_for_metric[:pass_rate] * 100).round %>%</strong></span>
46
+ <span class="ck-metric-pip__label"><%= metric.name %> <strong><%= ck_check_average(avg_for_metric) %></strong></span>
47
47
  </span>
48
48
  <% elsif avg_for_metric %>
49
49
  <span class="ck-metric-pip ck-metric-pip--<%= ck_score_kind(avg_for_metric[:avg]) %>">
@@ -56,18 +56,22 @@
56
56
  <td class="ck-compare-metric" data-label="Metric" title="<%= pm[:metric_name] %>"><%= pm[:metric_name] %></td>
57
57
  <td class="ck-cmp-score" data-label="A → B">
58
58
  <span class="ck-cmp-score__pair">
59
- <%= render "compare_score", kind: pm[:kind], passed: pm[:left_passed], score: pm[:left_score], response: row[:left_response], run: @run, side: "A" %>
59
+ <%= render "compare_score", kind: pm[:kind], passed: pm[:left_passed], score: pm[:left_score], check_score: pm[:left_check_score], response: row[:left_response], run: @run, side: "A" %>
60
60
  <span class="ck-cmp-score__arrow" aria-hidden="true">→</span>
61
- <%= render "compare_score", kind: pm[:kind], passed: pm[:right_passed], score: pm[:right_score], response: row[:right_response], run: @other_run, side: "B" %>
61
+ <%= render "compare_score", kind: pm[:kind], passed: pm[:right_passed], score: pm[:right_score], check_score: pm[:right_check_score], response: row[:right_response], run: @other_run, side: "B" %>
62
62
  </span>
63
63
  </td>
64
64
  <td class="ck-num ck-cmp-delta" data-label="Δ">
65
65
  <% if pm[:kind] == "check" %>
66
- <%= ck_result_change_badge(pm[:result_change]) || content_tag(:span, "—", class: "ck-cmp-score__none") %>
66
+ <% points = ck_check_score_change(pm[:left_check_score], pm[:right_check_score]) %>
67
+ <% badge = ck_result_change_badge(pm[:result_change]) unless points && pm[:result_change] == "same" %>
68
+ <% if badge || points %>
69
+ <span class="ck-cmp-delta__pair"><%= badge %><%= ck_delta_chip(points, "%") if points %></span>
70
+ <% else %>
71
+ <span class="ck-cmp-score__none">—</span>
72
+ <% end %>
67
73
  <% elsif pm[:delta] %>
68
- <% d = pm[:delta] %>
69
- <% dcls = d > 0 ? "ck-delta--positive" : d < 0 ? "ck-delta--negative" : "ck-delta--zero" %>
70
- <span class="ck-delta <%= dcls %>"><span class="ck-delta__arrow" aria-hidden="true"><%= d > 0 ? "▲" : d < 0 ? "▼" : "=" %></span><%= d > 0 ? "+#{d}" : d.to_s %></span>
74
+ <%= ck_delta_chip(pm[:delta]) %>
71
75
  <% else %>
72
76
  <span class="ck-cmp-score__none">—</span>
73
77
  <% end %>
@@ -2,7 +2,7 @@
2
2
  <div class="ck-select" data-ck-select>
3
3
  <input type="hidden" name="<%= name %>" value="<%= chosen && chosen.first %>" data-ck-select-value>
4
4
  <button type="button" class="ck-select__trigger" data-ck-select-trigger
5
- aria-haspopup="listbox" aria-expanded="false" aria-labelledby="<%= labelledby %>">
5
+ aria-haspopup="listbox" aria-expanded="false" aria-labelledby="<%= labelledby %>" <%= tag.attributes("aria-invalid": ("true" if local_assigns[:invalid])) %>>
6
6
  <span class="ck-select__label" data-ck-select-label><%= chosen && chosen.last %></span>
7
7
  <span class="ck-select__chevron" aria-hidden="true"><%= heroicon_tag "chevron-down" %></span>
8
8
  </button>
@@ -6,6 +6,8 @@ en:
6
6
  completion_kit/run:
7
7
  judge_model: Judge model
8
8
  metric_ids: Metrics
9
+ completion_kit/metric:
10
+ check_config: Check
9
11
  completion_kit/metric_group:
10
12
  metric_ids: Metrics
11
13
  completion_kit/provider_credential:
@@ -0,0 +1,5 @@
1
+ class AddCheckScoreToCompletionKitReviews < ActiveRecord::Migration[8.1]
2
+ def change
3
+ add_column :completion_kit_reviews, :check_score, :decimal, precision: 5, scale: 4
4
+ end
5
+ end
@@ -1,3 +1,3 @@
1
1
  module CompletionKit
2
- VERSION = "0.28.46"
2
+ VERSION = "0.29.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: completion-kit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.28.46
4
+ version: 0.29.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Damien Bastin
@@ -264,13 +264,20 @@ files:
264
264
  - app/services/completion_kit/checks/contains.rb
265
265
  - app/services/completion_kit/checks/equals.rb
266
266
  - app/services/completion_kit/checks/expected_resolver.rb
267
+ - app/services/completion_kit/checks/flag.rb
267
268
  - app/services/completion_kit/checks/json_path_equals.rb
269
+ - app/services/completion_kit/checks/labels.rb
268
270
  - app/services/completion_kit/checks/length_bounds.rb
271
+ - app/services/completion_kit/checks/list_overlap.rb
269
272
  - app/services/completion_kit/checks/not_contains.rb
273
+ - app/services/completion_kit/checks/numeric_bounds.rb
274
+ - app/services/completion_kit/checks/numeric_equals.rb
275
+ - app/services/completion_kit/checks/numeric_value.rb
270
276
  - app/services/completion_kit/checks/regex.rb
271
277
  - app/services/completion_kit/checks/registry.rb
272
278
  - app/services/completion_kit/checks/result.rb
273
279
  - app/services/completion_kit/checks/target_resolver.rb
280
+ - app/services/completion_kit/checks/textual.rb
274
281
  - app/services/completion_kit/checks/valid_json.rb
275
282
  - app/services/completion_kit/csv_processor.rb
276
283
  - app/services/completion_kit/dashboard_stats.rb
@@ -452,6 +459,7 @@ files:
452
459
  - db/migrate/20260730000002_create_completion_kit_prompt_serves.rb
453
460
  - db/migrate/20260804000001_add_judge_temperature_ignored_to_runs.rb
454
461
  - db/migrate/20260804000002_default_runs_to_no_temperature.rb
462
+ - db/migrate/20260903000001_add_check_score_to_completion_kit_reviews.rb
455
463
  - db/migrate/20261013000001_drop_completion_kit_mcp_sessions.rb
456
464
  - lib/completion-kit.rb
457
465
  - lib/completion_kit.rb