@malloy-publisher/skills 0.1.20 → 0.1.21
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/package.json +1 -1
- package/skills/eval-answer/SKILL.md +45 -5
- package/skills/eval-answer/reference/definition-ledger.md +305 -0
- package/skills/eval-answer/reference/ledger-schema.md +36 -7
- package/skills/eval-answer/scripts/check_coverage.py +12 -21
- package/skills/eval-answer/scripts/check_coverage_test.py +22 -0
- package/skills/eval-answer/scripts/check_findable.py +386 -0
- package/skills/eval-answer/scripts/check_findable_test.py +326 -0
- package/skills/eval-answer/scripts/json_scan.py +43 -0
- package/skills/eval-answer/scripts/ledger.py +63 -7
- package/skills/eval-answer/scripts/ledger_test.py +7 -0
- package/skills/eval-answer/scripts/mcp_payload.py +111 -2
- package/skills/eval-answer/scripts/mcp_payload_test.py +73 -2
- package/skills/eval-answer/scripts/publisher_rest.py +10 -4
- package/skills/eval-answer/scripts/publisher_rest_test.py +56 -0
- package/skills/eval-answer/scripts/score_retrieval.py +326 -41
- package/skills/eval-answer/scripts/score_retrieval_test.py +537 -15
- package/skills/eval-answer/scripts/verify_definitions.py +752 -0
- package/skills/eval-answer/scripts/verify_definitions_test.py +820 -0
- package/skills/eval-answer/scripts/verify_goldens.py +336 -22
- package/skills/eval-answer/scripts/verify_goldens_test.py +261 -2
- package/skills/eval-diagnose/SKILL.md +47 -2
- package/skills/eval-diagnose/scripts/cluster_failures.py +57 -14
- package/skills/eval-diagnose/scripts/cluster_failures_test.py +127 -0
- package/skills/eval-diagnose/scripts/diagnose.py +471 -42
- package/skills/eval-diagnose/scripts/diagnose_test.py +421 -0
- package/skills/eval-import/SKILL.md +180 -1
- package/skills/eval-import/reference/case-format.md +1 -1
- package/skills/eval-improve/SKILL.md +1 -1
- package/skills/eval-improve/scripts/improve.py +29 -3
- package/skills/eval-improve/scripts/improve_test.py +51 -0
- package/skills/eval-judge/SKILL.md +1 -1
- package/skills/eval-loop/SKILL.md +112 -3
- package/skills/eval-loop/reference/measurement.md +44 -4
- package/skills/eval-loop/reference/running-a-run.md +5 -1
- package/skills/eval-loop/scripts/agent_harness.py +21 -20
- package/skills/eval-loop/scripts/agent_harness_test.py +136 -0
- package/skills/eval-loop/scripts/build_run_package.py +9 -3
- package/skills/eval-loop/scripts/flip_table.py +107 -2
- package/skills/eval-loop/scripts/flip_table_test.py +165 -0
- package/skills/eval-loop/scripts/run_baseline.py +956 -97
- package/skills/eval-loop/scripts/run_baseline_test.py +799 -1
- package/skills/eval-loop/templates/eval-run-package/README.md +19 -5
- package/skills/eval-loop/templates/eval-run-package/eval_run.malloy +48 -19
- package/skills/eval-loop/templates/eval-run-package/eval_run.malloynb +11 -4
- package/skills/eval-loop/templates/eval-run-package/public/app.js +12 -4
- package/skills/eval-report/SKILL.md +362 -0
- package/skills/malloy/SKILL.md +2 -1
- package/skills/malloy-analysis/SKILL.md +1 -1
- package/skills/malloy-analysis-pitfalls/SKILL.md +13 -1
- package/skills/malloy-analysis-report/SKILL.md +1 -1
- package/skills/malloy-analyze/SKILL.md +1 -1
- package/skills/malloy-charts/SKILL.md +1 -1
- package/skills/malloy-dashboards/SKILL.md +1 -1
- package/skills/malloy-debug/SKILL.md +1 -1
- package/skills/malloy-define/SKILL.md +1 -1
- package/skills/malloy-discover/SKILL.md +1 -1
- package/skills/malloy-document/SKILL.md +1 -1
- package/skills/malloy-getting-started/SKILL.md +1 -1
- package/skills/malloy-gotchas-modeling/SKILL.md +1 -1
- package/skills/malloy-gotchas-queries/SKILL.md +1 -1
- package/skills/malloy-gotchas-rendering/SKILL.md +1 -1
- package/skills/malloy-html-data-app-embedding/SKILL.md +1 -1
- package/skills/malloy-html-data-app-runtime/SKILL.md +1 -1
- package/skills/malloy-html-data-apps/SKILL.md +1 -1
- package/skills/malloy-lookml-review/SKILL.md +1 -1
- package/skills/malloy-materialization/SKILL.md +1 -1
- package/skills/malloy-materialization-tuning/SKILL.md +1 -1
- package/skills/malloy-model/SKILL.md +1 -1
- package/skills/malloy-model-as-you-go/SKILL.md +1 -1
- package/skills/malloy-modeling/SKILL.md +1 -1
- package/skills/malloy-notebook-chat/SKILL.md +1 -1
- package/skills/malloy-notebooks/SKILL.md +1 -1
- package/skills/malloy-patterns/SKILL.md +1 -1
- package/skills/malloy-phrase-detection/SKILL.md +3 -2
- package/skills/malloy-publish/SKILL.md +1 -1
- package/skills/malloy-queries/SKILL.md +1 -1
- package/skills/malloy-review/SKILL.md +1 -1
- package/skills/malloy-scope/SKILL.md +1 -1
|
@@ -211,5 +211,426 @@ class OffVocabularyValuesDoNotLoseTheRun(unittest.TestCase):
|
|
|
211
211
|
improve_py)
|
|
212
212
|
|
|
213
213
|
|
|
214
|
+
class BehaviourStats(unittest.TestCase):
|
|
215
|
+
"""The measurements a behavioural root cause gets stated in.
|
|
216
|
+
|
|
217
|
+
Diagnosis reads failures only, so a behaviour common to the whole run looks
|
|
218
|
+
causal from inside it. On one measured run the largest cluster was built on
|
|
219
|
+
"substitutes broad enumeration for targeted retrieval", and the enumeration
|
|
220
|
+
rate was 25% of targets in the failures against 26% in the passes.
|
|
221
|
+
|
|
222
|
+
THE FIXTURES HERE ARE THE SHAPES THE HARNESS ACTUALLY WRITES. An earlier
|
|
223
|
+
version of this class invented a `{"target_type", "search_text"}` dict that
|
|
224
|
+
no writer emits, and the code passed those tests while reading
|
|
225
|
+
`targetsWithoutSearchText: 0` on every real attempt -- because `targets`
|
|
226
|
+
holds `"<type>: <text>"` STRINGS and a target with no text is dropped
|
|
227
|
+
before the ledger sees it.
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
def events(self, qid, tool_call, *, verdict="match", turns=9):
|
|
231
|
+
return [
|
|
232
|
+
{"kind": "attempt", "qid": qid, "n_get_context": 2, "n_execute": 5,
|
|
233
|
+
"n_execute_errors": 1, "num_turns": turns,
|
|
234
|
+
"skills_invoked": ["malloy-analysis"]},
|
|
235
|
+
{"kind": "score", "qid": qid, "verdict": verdict},
|
|
236
|
+
{"kind": "tool_call", "qid": qid, "tool": "get_context",
|
|
237
|
+
**tool_call},
|
|
238
|
+
{"kind": "tool_call", "qid": qid, "tool": "execute_query"},
|
|
239
|
+
]
|
|
240
|
+
|
|
241
|
+
def shapes(self, *pairs):
|
|
242
|
+
return {"target_shapes": [{"type": t, "has_text": h} for t, h in pairs]}
|
|
243
|
+
|
|
244
|
+
def test_a_bare_target_is_counted_from_target_shapes(self):
|
|
245
|
+
s = diagnose.behaviour_stats("q1", self.events("q1", self.shapes(
|
|
246
|
+
("dimension", False), ("measure", True), ("view", False))))
|
|
247
|
+
self.assertEqual(s["searchTargets"], 3)
|
|
248
|
+
self.assertEqual(s["targetsWithoutSearchText"], 2)
|
|
249
|
+
self.assertTrue(s["targetsMeasured"])
|
|
250
|
+
|
|
251
|
+
def test_it_records_which_target_types_were_asked_for(self):
|
|
252
|
+
# A type-classification error is the most common agent-call defect, and
|
|
253
|
+
# a falsifier that cannot reach it is not much of a falsifier -- which
|
|
254
|
+
# a clustering agent said, in those terms, about the first version.
|
|
255
|
+
s = diagnose.behaviour_stats("q1", self.events("q1", self.shapes(
|
|
256
|
+
("measure", True), ("dimension", True), ("dimension", False))))
|
|
257
|
+
self.assertEqual(s["targetTypes"], {"measure": 1, "dimension": 2})
|
|
258
|
+
|
|
259
|
+
def test_a_legacy_run_reads_types_but_abstains_on_the_bare_count(self):
|
|
260
|
+
# Runs written before `target_shapes` existed. The types survive in the
|
|
261
|
+
# `"<type>: <text>"` strings; the bare count does not, and reporting it
|
|
262
|
+
# as 0 would be a falsifier quietly asserting the opposite of the truth.
|
|
263
|
+
s = diagnose.behaviour_stats("q1", self.events(
|
|
264
|
+
"q1", {"targets": ["measure: total revenue", "source: flights"]}))
|
|
265
|
+
self.assertEqual(s["targetTypes"], {"measure": 1, "source": 1})
|
|
266
|
+
self.assertIsNone(s["targetsWithoutSearchText"])
|
|
267
|
+
self.assertFalse(s["targetsMeasured"])
|
|
268
|
+
|
|
269
|
+
def test_the_two_shapes_do_not_get_mixed(self):
|
|
270
|
+
# If both are present the measured one wins; the legacy strings are a
|
|
271
|
+
# lossy view of the same call.
|
|
272
|
+
s = diagnose.behaviour_stats("q1", self.events("q1", {
|
|
273
|
+
"targets": ["measure: total revenue"],
|
|
274
|
+
**self.shapes(("measure", True), ("dimension", False))}))
|
|
275
|
+
self.assertEqual(s["searchTargets"], 2)
|
|
276
|
+
self.assertEqual(s["targetsWithoutSearchText"], 1)
|
|
277
|
+
|
|
278
|
+
def test_it_carries_the_verdict_so_the_groups_are_comparable(self):
|
|
279
|
+
s = diagnose.behaviour_stats("q1", self.events("q1", self.shapes(),
|
|
280
|
+
verdict="match"))
|
|
281
|
+
self.assertEqual(s["verdict"], "match")
|
|
282
|
+
self.assertEqual(s["skillsInvoked"], ["malloy-analysis"])
|
|
283
|
+
self.assertEqual(s["numTurns"], 9)
|
|
284
|
+
|
|
285
|
+
def test_only_this_case_is_measured(self):
|
|
286
|
+
ev = self.events("q1", self.shapes(("dimension", True))) + \
|
|
287
|
+
self.events("q2", self.shapes(("a", True), ("b", True)))
|
|
288
|
+
self.assertEqual(
|
|
289
|
+
diagnose.behaviour_stats("q1", ev)["searchTargets"], 1)
|
|
290
|
+
|
|
291
|
+
def test_an_attempt_with_no_calls_is_still_a_row(self):
|
|
292
|
+
s = diagnose.behaviour_stats("q9", [])
|
|
293
|
+
self.assertEqual(s["qid"], "q9")
|
|
294
|
+
self.assertEqual(s["searchTargets"], 0)
|
|
295
|
+
|
|
296
|
+
def test_the_clustering_prompt_asks_for_the_falsification(self):
|
|
297
|
+
self.assertIn("{controls}", diagnose.CLUSTER_PROMPT)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
class ControlsBlock(unittest.TestCase):
|
|
301
|
+
"""The CONTROLS section sizes itself, because the instruction that is right
|
|
302
|
+
for twenty passing cases is wrong for one and meaningless for none."""
|
|
303
|
+
|
|
304
|
+
def block(self, n):
|
|
305
|
+
return diagnose.controls_block(
|
|
306
|
+
[{"qid": f"q{i}", "verdict": "match"} for i in range(n)])
|
|
307
|
+
|
|
308
|
+
def test_a_run_where_everything_failed_says_there_is_nothing_to_compare(self):
|
|
309
|
+
# 10 of 10 failing is a real shape, and asking the agent to compare
|
|
310
|
+
# rates against an empty list invites it to read [] as evidence.
|
|
311
|
+
b = self.block(0)
|
|
312
|
+
self.assertIn("CONTROLS: none", b)
|
|
313
|
+
self.assertIn("unfalsified", b)
|
|
314
|
+
self.assertIn("Do NOT read the absence of controls", b)
|
|
315
|
+
# A model-fact cluster is not affected by having no controls, and the
|
|
316
|
+
# block has to say so or every cluster gets downgraded.
|
|
317
|
+
self.assertIn("unaffected", b)
|
|
318
|
+
|
|
319
|
+
def test_one_or_two_controls_is_not_a_rate(self):
|
|
320
|
+
for n in (1, 2):
|
|
321
|
+
with self.subTest(n=n):
|
|
322
|
+
b = self.block(n)
|
|
323
|
+
self.assertIn("is NOT a rate", b)
|
|
324
|
+
self.assertIn("Do not compute a percentage", b)
|
|
325
|
+
|
|
326
|
+
def test_three_or_more_gets_the_real_comparison(self):
|
|
327
|
+
b = self.block(3)
|
|
328
|
+
self.assertIn("similar rate in the passes", b)
|
|
329
|
+
self.assertNotIn("is NOT a rate", b)
|
|
330
|
+
|
|
331
|
+
def test_the_prompt_renders_at_every_size(self):
|
|
332
|
+
# The failure this guards: an unrendered {controls} shipping to the
|
|
333
|
+
# agent, or a KeyError at the one moment the run cannot be redone.
|
|
334
|
+
for n in (0, 1, 2, 3, 20):
|
|
335
|
+
with self.subTest(n=n):
|
|
336
|
+
out = diagnose.CLUSTER_PROMPT.format(
|
|
337
|
+
issues="[]", controls=self.block(n))
|
|
338
|
+
self.assertNotIn("{controls}", out)
|
|
339
|
+
self.assertNotIn("{issues}", out)
|
|
340
|
+
|
|
341
|
+
def test_every_size_carries_the_rows_it_has(self):
|
|
342
|
+
self.assertIn('"qid": "q0"', self.block(1))
|
|
343
|
+
self.assertNotIn('"qid"', self.block(0))
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
class RetrievalMissOnAPassingCase(unittest.TestCase):
|
|
347
|
+
"""A correct answer that never received a required entity is a finding.
|
|
348
|
+
|
|
349
|
+
It was silently dropped in three places: `attribute()` short-circuited on
|
|
350
|
+
`passed`, `summarise()` dropped the empty label, and `diagnose.py` routed
|
|
351
|
+
`match` to a control group. Measured on one run, 3 of 4 findings sat on
|
|
352
|
+
passing cases and produced nothing.
|
|
353
|
+
"""
|
|
354
|
+
|
|
355
|
+
def case(self, qid="q", required=("measure:m:total",)):
|
|
356
|
+
return {"qid": qid, "coverage": "covered",
|
|
357
|
+
"expectedEntities": {"required": list(required)}}
|
|
358
|
+
|
|
359
|
+
def events(self, qid, returned, targets):
|
|
360
|
+
return [
|
|
361
|
+
{"kind": "attempt", "qid": qid, "sample": None, "phase": "baseline"},
|
|
362
|
+
{"kind": "score", "qid": qid, "sample": None, "phase": "baseline",
|
|
363
|
+
"verdict": "match", "reason": "ok"},
|
|
364
|
+
{"kind": "tool_call", "qid": qid, "sample": None, "phase": "baseline",
|
|
365
|
+
"tool": "get_context",
|
|
366
|
+
"rankedSummary": {"entityIds": list(returned)},
|
|
367
|
+
"target_shapes": [{"type": t, "has_text": True} for t in targets]},
|
|
368
|
+
]
|
|
369
|
+
|
|
370
|
+
def test_a_pass_that_missed_an_entity_yields_a_finding(self):
|
|
371
|
+
ev = self.events("q", [], ["source", "dimension"])
|
|
372
|
+
row = diagnose.retrieval_finding(self.case(), ev,
|
|
373
|
+
("q", None, "baseline"), "match")
|
|
374
|
+
self.assertIsNotNone(row)
|
|
375
|
+
# Deterministic: it needed a measure and sent no measure target.
|
|
376
|
+
self.assertIn("never asked", row["where_to_fix"])
|
|
377
|
+
# And the answer is still not a failure.
|
|
378
|
+
self.assertFalse(row["failed"])
|
|
379
|
+
|
|
380
|
+
def test_a_pass_that_received_everything_yields_nothing(self):
|
|
381
|
+
ev = self.events("q", ["measure:m:total"], ["measure"])
|
|
382
|
+
self.assertIsNone(diagnose.retrieval_finding(
|
|
383
|
+
self.case(), ev, ("q", None, "baseline"), "match"))
|
|
384
|
+
|
|
385
|
+
def test_it_calls_the_real_scorer_rather_than_restating_the_rule(self):
|
|
386
|
+
# Two definitions of "a miss" would drift, and the run summary and the
|
|
387
|
+
# diagnosis would then disagree about what happened.
|
|
388
|
+
src = pathlib.Path(diagnose.__file__).read_text()
|
|
389
|
+
self.assertIn("score_retrieval.score_case", src)
|
|
390
|
+
|
|
391
|
+
def test_the_prompt_says_the_answer_was_correct(self):
|
|
392
|
+
# Without it the diagnoser reads the evidence as a wrong answer and
|
|
393
|
+
# goes looking for a number that is not wrong.
|
|
394
|
+
src = pathlib.Path(diagnose.__file__).read_text()
|
|
395
|
+
self.assertIn("THIS ANSWER WAS CORRECT", src)
|
|
396
|
+
self.assertIn("answered_correctly", src)
|
|
397
|
+
|
|
398
|
+
def test_there_is_an_opt_out(self):
|
|
399
|
+
src = pathlib.Path(diagnose.__file__).read_text()
|
|
400
|
+
self.assertIn("--no-retrieval-misses", src)
|
|
401
|
+
|
|
402
|
+
def test_the_coverage_denominator_excludes_them(self):
|
|
403
|
+
# They are passes. Counting a diagnosed pass against a non-passing
|
|
404
|
+
# denominator printed "2 of 1 non-passing case(s) diagnosed (200%)".
|
|
405
|
+
src = pathlib.Path(diagnose.__file__).read_text()
|
|
406
|
+
self.assertIn("good_failures", src)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
class SelectingWhatToDiagnose(unittest.TestCase):
|
|
410
|
+
"""Every scored case lands in exactly one bucket, and narrowing what gets
|
|
411
|
+
diagnosed does not change what the run failed."""
|
|
412
|
+
|
|
413
|
+
def cases(self, n, split=None):
|
|
414
|
+
return {f"q{i}": {"qid": f"q{i}", "coverage": "covered",
|
|
415
|
+
**({"split": split} if split else {})}
|
|
416
|
+
for i in range(n)}
|
|
417
|
+
|
|
418
|
+
def scores(self, n, verdict="no_match"):
|
|
419
|
+
return [{"kind": "score", "qid": f"q{i}", "sample": None,
|
|
420
|
+
"phase": "baseline", "verdict": verdict, "reason": "r"}
|
|
421
|
+
for i in range(n)]
|
|
422
|
+
|
|
423
|
+
def select(self, events, cases, **kw):
|
|
424
|
+
"""(failed, passed, retrieval_only, excluded) -- the four buckets most
|
|
425
|
+
of these assert on. `excluded_passes` has its own tests below."""
|
|
426
|
+
return diagnose.select_cases(events, cases, ("no_match",), **kw)[:4]
|
|
427
|
+
|
|
428
|
+
def test_limit_records_what_it_dropped_as_an_exclusion(self):
|
|
429
|
+
# Truncating `failed` in place moved numerator and denominator
|
|
430
|
+
# together: 23 failures with --limit 5 printed "5 of 5 (100%)".
|
|
431
|
+
failed, _, _, excluded = self.select(
|
|
432
|
+
self.scores(23), self.cases(23), limit=5)
|
|
433
|
+
self.assertEqual(len(failed), 5)
|
|
434
|
+
not_passing = len(failed) + sum(len(v) for v in excluded.values())
|
|
435
|
+
self.assertEqual(not_passing, 23)
|
|
436
|
+
self.assertEqual(len(excluded["beyond --limit 5"]), 18)
|
|
437
|
+
|
|
438
|
+
def test_limit_zero_selects_nothing_rather_than_everything(self):
|
|
439
|
+
"""`if limit:` read 0 as "no limit" and diagnosed all 23, one billable
|
|
440
|
+
agent each -- failing OPEN on the one axis where that costs money.
|
|
441
|
+
0 means zero; unlimited is spelled by omitting the flag."""
|
|
442
|
+
failed, _, _, excluded = self.select(
|
|
443
|
+
self.scores(23), self.cases(23), limit=0)
|
|
444
|
+
self.assertEqual(failed, [])
|
|
445
|
+
self.assertEqual(len(excluded["beyond --limit 0"]), 23)
|
|
446
|
+
|
|
447
|
+
def test_omitting_the_limit_still_means_unlimited(self):
|
|
448
|
+
failed, _, _, excluded = self.select(self.scores(23), self.cases(23))
|
|
449
|
+
self.assertEqual(len(failed), 23)
|
|
450
|
+
self.assertEqual(excluded, {})
|
|
451
|
+
|
|
452
|
+
def test_a_holdout_PASS_is_not_a_non_passing_case(self):
|
|
453
|
+
"""The invariant, for the branch that still broke it.
|
|
454
|
+
|
|
455
|
+
The holdout test sits above the verdict checks on purpose, so the
|
|
456
|
+
split cannot depend on the score. That sent a holdout case that
|
|
457
|
+
PASSED into `excluded`, which `not_passing` sums: a real run with one
|
|
458
|
+
failure and four holdout passes printed "1 of 5 non-passing case(s)
|
|
459
|
+
diagnosed (20%)" when it had diagnosed the only failure there was.
|
|
460
|
+
"""
|
|
461
|
+
events = self.scores(1, verdict="no_match") + [
|
|
462
|
+
{"kind": "score", "qid": f"h{i}", "sample": None,
|
|
463
|
+
"phase": "baseline", "verdict": "match", "reason": "r"}
|
|
464
|
+
for i in range(4)]
|
|
465
|
+
cases = dict(self.cases(1))
|
|
466
|
+
for i in range(4):
|
|
467
|
+
cases[f"h{i}"] = {"qid": f"h{i}", "split": "holdout"}
|
|
468
|
+
failed, _, _, excluded, excluded_passes = diagnose.select_cases(
|
|
469
|
+
events, cases, ("no_match",))
|
|
470
|
+
self.assertEqual(failed, ["q0"])
|
|
471
|
+
not_passing = len(failed) + sum(len(v) for v in excluded.values())
|
|
472
|
+
self.assertEqual(not_passing, 1, "four holdout PASSES inflated it to 5")
|
|
473
|
+
self.assertEqual(
|
|
474
|
+
len(excluded_passes["holdout, withheld from diagnosis"]), 4)
|
|
475
|
+
|
|
476
|
+
def test_a_holdout_FAILURE_is_still_a_non_passing_case(self):
|
|
477
|
+
events = [{"kind": "score", "qid": "h0", "sample": None,
|
|
478
|
+
"phase": "baseline", "verdict": "no_match", "reason": "r"}]
|
|
479
|
+
cases = {"h0": {"qid": "h0", "split": "holdout"}}
|
|
480
|
+
failed, _, _, excluded, excluded_passes = diagnose.select_cases(
|
|
481
|
+
events, cases, ("no_match",))
|
|
482
|
+
self.assertEqual(failed, [])
|
|
483
|
+
self.assertEqual(excluded_passes, {})
|
|
484
|
+
self.assertEqual(
|
|
485
|
+
len(excluded["holdout, withheld from diagnosis"]), 1,
|
|
486
|
+
"a holdout failure is still withheld, and still non-passing")
|
|
487
|
+
|
|
488
|
+
def test_only_records_what_it_dropped_as_an_exclusion(self):
|
|
489
|
+
failed, _, _, excluded = self.select(
|
|
490
|
+
self.scores(4), self.cases(4), only="q0,q1")
|
|
491
|
+
self.assertEqual(sorted(failed), ["q0", "q1"])
|
|
492
|
+
self.assertEqual(
|
|
493
|
+
len(failed) + sum(len(v) for v in excluded.values()), 4)
|
|
494
|
+
|
|
495
|
+
def test_an_unnarrowed_run_excludes_nothing_for_narrowing(self):
|
|
496
|
+
failed, _, _, excluded = self.select(self.scores(3), self.cases(3))
|
|
497
|
+
self.assertEqual(len(failed), 3)
|
|
498
|
+
self.assertEqual(excluded, {})
|
|
499
|
+
|
|
500
|
+
def test_a_passing_holdout_with_a_retrieval_miss_stays_withheld(self):
|
|
501
|
+
"""The leak: `verdict == "match"` was tested BEFORE the split, so a
|
|
502
|
+
holdout case that answered correctly with an incomplete retrieval went
|
|
503
|
+
into `retrieval_only` and on to a diagnosis call on every run, with no
|
|
504
|
+
flag able to prevent it. Holdout exists so improve has something
|
|
505
|
+
diagnosis never saw."""
|
|
506
|
+
cases = {"q0": {"qid": "q0", "coverage": "covered", "split": "holdout",
|
|
507
|
+
"expectedEntities": {"required": ["measure:m:total"]}}}
|
|
508
|
+
events = [
|
|
509
|
+
{"kind": "score", "qid": "q0", "sample": None, "phase": "baseline",
|
|
510
|
+
"verdict": "match", "reason": "ok"},
|
|
511
|
+
{"kind": "tool_call", "qid": "q0", "sample": None,
|
|
512
|
+
"phase": "baseline", "tool": "get_context",
|
|
513
|
+
"rankedSummary": {"entityIds": []},
|
|
514
|
+
"target_shapes": [{"type": "dimension", "has_text": True}]},
|
|
515
|
+
]
|
|
516
|
+
(failed, passed, retrieval_only, excluded,
|
|
517
|
+
excluded_passes) = diagnose.select_cases(events, cases, ("no_match",))
|
|
518
|
+
self.assertEqual(retrieval_only, [])
|
|
519
|
+
self.assertEqual(passed, [])
|
|
520
|
+
self.assertEqual(failed, [])
|
|
521
|
+
# Withheld, which is what this test is about -- and in the PASSES
|
|
522
|
+
# bucket, because it matched and `not_passing` sums the other one.
|
|
523
|
+
self.assertIn("q0",
|
|
524
|
+
excluded_passes["holdout, withheld from diagnosis"])
|
|
525
|
+
self.assertEqual(excluded, {})
|
|
526
|
+
|
|
527
|
+
def test_include_holdout_lets_that_same_case_through(self):
|
|
528
|
+
cases = {"q0": {"qid": "q0", "coverage": "covered", "split": "holdout",
|
|
529
|
+
"expectedEntities": {"required": ["measure:m:total"]}}}
|
|
530
|
+
events = [
|
|
531
|
+
{"kind": "score", "qid": "q0", "sample": None, "phase": "baseline",
|
|
532
|
+
"verdict": "match", "reason": "ok"},
|
|
533
|
+
{"kind": "tool_call", "qid": "q0", "sample": None,
|
|
534
|
+
"phase": "baseline", "tool": "get_context",
|
|
535
|
+
"rankedSummary": {"entityIds": []},
|
|
536
|
+
"target_shapes": [{"type": "dimension", "has_text": True}]},
|
|
537
|
+
]
|
|
538
|
+
_, passed, retrieval_only, _ = self.select(
|
|
539
|
+
events, cases, include_holdout=True)
|
|
540
|
+
self.assertEqual(passed, ["q0"])
|
|
541
|
+
self.assertEqual(retrieval_only, ["q0"])
|
|
542
|
+
|
|
543
|
+
def passing_with_a_miss(self):
|
|
544
|
+
"""Three cases that all PASSED, two of them on an incomplete
|
|
545
|
+
retrieval."""
|
|
546
|
+
cases, ev = {}, []
|
|
547
|
+
for q in ("q1", "q2", "q3"):
|
|
548
|
+
cases[q] = {"qid": q, "coverage": "covered",
|
|
549
|
+
"expectedEntities": {"required": ["measure:m:total"]}}
|
|
550
|
+
got = [] if q in ("q1", "q2") else ["measure:m:total"]
|
|
551
|
+
ev += [{"kind": "score", "qid": q, "sample": None,
|
|
552
|
+
"phase": "baseline", "verdict": "match", "reason": "ok"},
|
|
553
|
+
{"kind": "tool_call", "qid": q, "sample": None,
|
|
554
|
+
"phase": "baseline", "tool": "get_context",
|
|
555
|
+
"rankedSummary": {"entityIds": got},
|
|
556
|
+
"target_shapes": [{"type": "measure", "has_text": True}]}]
|
|
557
|
+
return cases, ev
|
|
558
|
+
|
|
559
|
+
def test_a_pass_kept_out_of_diagnosis_is_not_a_non_passing_case(self):
|
|
560
|
+
"""`--no-retrieval-misses` used to push these into `excluded`, which
|
|
561
|
+
`not_passing` sums, so a run where every case passed printed
|
|
562
|
+
"coverage: 0 of 2 non-passing case(s) diagnosed (0%)"."""
|
|
563
|
+
cases, ev = self.passing_with_a_miss()
|
|
564
|
+
f, p, r, excluded, excluded_passes = diagnose.select_cases(
|
|
565
|
+
ev, cases, ("no_match",), no_retrieval_misses=True)
|
|
566
|
+
self.assertEqual(f, [])
|
|
567
|
+
self.assertEqual(sorted(p), ["q1", "q2", "q3"])
|
|
568
|
+
self.assertEqual(excluded, {})
|
|
569
|
+
not_passing = len(f) + sum(len(v) for v in excluded.values())
|
|
570
|
+
self.assertEqual(not_passing, 0)
|
|
571
|
+
# Still reported, just not as a failure.
|
|
572
|
+
self.assertEqual(
|
|
573
|
+
excluded_passes["passed with a retrieval miss "
|
|
574
|
+
"(--no-retrieval-misses)"], ["q1", "q2"])
|
|
575
|
+
|
|
576
|
+
def test_without_the_flag_those_passes_are_diagnosed(self):
|
|
577
|
+
cases, ev = self.passing_with_a_miss()
|
|
578
|
+
f, p, r, excluded, excluded_passes = diagnose.select_cases(
|
|
579
|
+
ev, cases, ("no_match",))
|
|
580
|
+
self.assertEqual(sorted(r), ["q1", "q2"])
|
|
581
|
+
self.assertEqual(excluded_passes, {})
|
|
582
|
+
self.assertEqual(len(f) + sum(len(v) for v in excluded.values()), 0)
|
|
583
|
+
|
|
584
|
+
def test_contamination_still_outranks_the_split(self):
|
|
585
|
+
cases = self.cases(1, split="holdout")
|
|
586
|
+
events = [{"kind": "score", "qid": "q0", "sample": None,
|
|
587
|
+
"phase": "baseline", "verdict": "no_match", "reason": "r",
|
|
588
|
+
"contaminated": True}]
|
|
589
|
+
_, _, _, excluded = self.select(events, cases)
|
|
590
|
+
self.assertIn("contaminated", excluded)
|
|
591
|
+
self.assertNotIn("holdout, withheld from diagnosis", excluded)
|
|
592
|
+
|
|
593
|
+
class MatchedPair(unittest.TestCase):
|
|
594
|
+
"""A case that passed in another arm is the richest evidence in the run,
|
|
595
|
+
and it was the one thing the diagnoser never saw.
|
|
596
|
+
|
|
597
|
+
diagnose.py reads one run. A flip lives across two, so a case that passed
|
|
598
|
+
once and failed once was diagnosed from the failing side alone -- with the
|
|
599
|
+
matched pair, same question and same model, sitting unread in the other
|
|
600
|
+
run directory.
|
|
601
|
+
"""
|
|
602
|
+
|
|
603
|
+
def evidence(self, passed_elsewhere=None):
|
|
604
|
+
events = [
|
|
605
|
+
{"kind": "attempt", "qid": "q1",
|
|
606
|
+
"final_query": "run: wrong -> { x }", "answer_text": "2 rows"},
|
|
607
|
+
{"kind": "score", "qid": "q1", "verdict": "no_match",
|
|
608
|
+
"reason": "expected 443"},
|
|
609
|
+
]
|
|
610
|
+
return diagnose.evidence_for("q1", {"question": "how many?"}, events,
|
|
611
|
+
passed_elsewhere)
|
|
612
|
+
|
|
613
|
+
def test_absent_by_default(self):
|
|
614
|
+
self.assertIsNone(self.evidence()["passedInAnotherArm"])
|
|
615
|
+
|
|
616
|
+
def test_the_other_arms_query_reaches_the_agent(self):
|
|
617
|
+
e = self.evidence({"arm": "aa-2", "verdict": "match",
|
|
618
|
+
"finalQuery": "run: right -> { y }"})
|
|
619
|
+
self.assertEqual(e["passedInAnotherArm"]["finalQuery"],
|
|
620
|
+
"run: right -> { y }")
|
|
621
|
+
self.assertEqual(e["passedInAnotherArm"]["arm"], "aa-2")
|
|
622
|
+
|
|
623
|
+
def test_this_arms_query_is_still_there_to_diff_against(self):
|
|
624
|
+
e = self.evidence({"arm": "aa-2", "finalQuery": "run: right -> { y }"})
|
|
625
|
+
self.assertIn("run: wrong -> { x }", e["queriesRun"])
|
|
626
|
+
|
|
627
|
+
def test_the_prompt_tells_the_agent_what_to_do_with_it(self):
|
|
628
|
+
self.assertIn("passedInAnotherArm", diagnose.DIAGNOSE_PROMPT)
|
|
629
|
+
self.assertIn("matched pair", diagnose.DIAGNOSE_PROMPT)
|
|
630
|
+
|
|
631
|
+
def test_the_prompt_says_a_flip_is_not_noise(self):
|
|
632
|
+
# The doctrine change, pinned: a flip is a model-quality finding.
|
|
633
|
+
self.assertIn("not noise", diagnose.DIAGNOSE_PROMPT)
|
|
634
|
+
|
|
214
635
|
if __name__ == "__main__":
|
|
215
636
|
unittest.main()
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: eval-import
|
|
3
3
|
description: 'Turn a list of questions into an eval set, whatever shape it arrived in: a JSONL a customer sent, a CSV, a spreadsheet export, a markdown doc, an email thread, or a pull from production logs. Classify each item by what came WITH the question (a query, a number, prose criteria, or nothing), write cases.jsonl and set.json per reference/case-format.md, keep the file as it arrived, and seal each question so a later edit is detectable. Never marks a value-bearing golden verified and never invents a question. Use when questions arrive from outside and need to become a set, or before the first run against a set nobody here authored.'
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.21
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Import questions into an eval set
|
|
@@ -95,6 +95,134 @@ back until somebody derives keys. A set of bare questions already measures
|
|
|
95
95
|
whether the model can express an answer at all, and the answers it produces
|
|
96
96
|
are what the keys get derived from.
|
|
97
97
|
|
|
98
|
+
### Deriving a key yourself, when nothing arrived with the question
|
|
99
|
+
|
|
100
|
+
The table above is about what ARRIVED. Sometimes nothing did, nobody knows the
|
|
101
|
+
right number, and the set still needs keys. You may derive one, and the rules
|
|
102
|
+
are the ones already stated rather than new ones:
|
|
103
|
+
|
|
104
|
+
- Use your full model access and query until you can defend the number. The
|
|
105
|
+
agent that gets tested later holds only `get_context` and `execute_query`;
|
|
106
|
+
you are not that agent, and the gap is the point.
|
|
107
|
+
- Record the query as `canonicalQuery`. A number with no query is somebody's
|
|
108
|
+
guess, which is the row above it in the table.
|
|
109
|
+
- Record the entities you used to get there as `expectedEntities.required`, in
|
|
110
|
+
the `kind:source:name` form: the measures, dimensions and views the answer
|
|
111
|
+
cannot be produced without. `reference/case-format.md` says to leave this
|
|
112
|
+
field empty at import because guessing it invents a retrieval expectation
|
|
113
|
+
nobody stated; you are not guessing, you just used them. That is the only
|
|
114
|
+
trustworthy producer this field has, and it is what makes retrieval
|
|
115
|
+
measurable on a set nobody sent keys for. Where the model offers two
|
|
116
|
+
legitimate routes, write a `requiredAnyOf` group rather than pick one.
|
|
117
|
+
`verify_goldens.py` check 5 audits every id against the model, so a wrong one
|
|
118
|
+
is a hard finding rather than a silent retrieval miss.
|
|
119
|
+
- **Derive the list with the model in front of you, not from memory, and not
|
|
120
|
+
with grep.** The model knows what each field IS; an author does not reliably.
|
|
121
|
+
Ask the question of the model: which entities does an answer to this depend
|
|
122
|
+
on, and is each a measure or a dimension. Confirm every id with `get_context`
|
|
123
|
+
before writing it. A list written from recollection is the most common way a
|
|
124
|
+
set acquires a retrieval expectation nobody can satisfy.
|
|
125
|
+
|
|
126
|
+
Measured: an agent given the model and one question found alternatives a
|
|
127
|
+
hand-written key had missed on all three cases tried -- carrier name against
|
|
128
|
+
nickname against code, `destination_count` against its underlying
|
|
129
|
+
`destination.airport_count`. At roughly $0.13 a question. It is better than a
|
|
130
|
+
person working from notes at finding what the model actually offers.
|
|
131
|
+
|
|
132
|
+
It is worse at deciding what to REQUIRE, and the two rules below are why. On
|
|
133
|
+
the same three cases it proposed demoting two named measures on the grounds
|
|
134
|
+
that a raw expression answered identically. One of those equivalences was
|
|
135
|
+
false; the other was true today and is not a reason to demote anything.
|
|
136
|
+
|
|
137
|
+
- **But VERIFY every "this is not strictly required" claim by running both
|
|
138
|
+
sides.** This is the rule that makes the step above safe, and it is not
|
|
139
|
+
optional.
|
|
140
|
+
|
|
141
|
+
The same probe got one badly wrong. Told that a named measure is not required
|
|
142
|
+
when a raw column plus a plain aggregate answers identically, it wrote that
|
|
143
|
+
`average_plane_size` was "equally answerable by
|
|
144
|
+
`avg(aircraft.aircraft_models.seats)`". Executed, those are 229 and 196.93.
|
|
145
|
+
The measure is `aircraft.avg(...)`, an average over distinct aircraft, and the
|
|
146
|
+
inline version averages over flights. The claim was false for exactly the
|
|
147
|
+
measure whose entire purpose is to carry that distinction -- and it is the
|
|
148
|
+
same mistake, on the same field, that produced the only wrong answer in the
|
|
149
|
+
run this set came from.
|
|
150
|
+
|
|
151
|
+
Had that proposal been written into the key, the case would have stopped
|
|
152
|
+
testing the thing it exists to test.
|
|
153
|
+
|
|
154
|
+
So: an equivalence is a claim about DATA, and reading a definition is not
|
|
155
|
+
evidence. Run both expressions and compare the rows.
|
|
156
|
+
|
|
157
|
+
**But agreeing today is not a reason to demote the model's entity, and this
|
|
158
|
+
is the part the probe got backwards.** `airport_count is count()` returns the
|
|
159
|
+
same 984 as a bare `count()` right now, and stops doing so the moment anyone
|
|
160
|
+
adds a null filter or a deduplication to it. The key would not notice: it
|
|
161
|
+
would go on passing while testing nothing. That applies to every measure that
|
|
162
|
+
is currently a thin wrapper, which is most of them until the day one is not.
|
|
163
|
+
|
|
164
|
+
**`requiredAnyOf` is for two routes the MODEL defines** -- a carrier's `name`
|
|
165
|
+
and its `nickname`, `destination_count` and the `destination.airport_count`
|
|
166
|
+
it wraps. Both are entities, both are retrievable, and an answer using either
|
|
167
|
+
has gone through the semantic layer. That is a real choice and the group
|
|
168
|
+
records it.
|
|
169
|
+
|
|
170
|
+
It is NOT for "the model's entity, or a raw expression that happens to
|
|
171
|
+
agree". Those are not two routes: one is the route, the other is a bypass
|
|
172
|
+
that currently works. Keeping the named entity in `required` is what makes
|
|
173
|
+
the eval measure what the semantic layer is for, and finding it IS the
|
|
174
|
+
retrieval behaviour under test -- an agent that searches for the pre-defined
|
|
175
|
+
entity inherits its definition and its rules, while one that rebuilds from
|
|
176
|
+
raw columns inherits whatever it thought of.
|
|
177
|
+
|
|
178
|
+
What the equivalence check is FOR is reading a run afterwards. When an answer
|
|
179
|
+
came out right without the entity, comparing the two says whether it got
|
|
180
|
+
lucky or got it wrong, and a set whose passes are merely lucky is a set of
|
|
181
|
+
near-misses.
|
|
182
|
+
|
|
183
|
+
- **Then prove each one is retrievable, and record what finds it.**
|
|
184
|
+
`check_findable.py --set <set> --mcp-url <mcp> --publisher <rest>
|
|
185
|
+
--environment <env> --package <pkg>` runs two checks and needs no model call.
|
|
186
|
+
With `--publisher` it reads the COMPILED model and settles whether each field
|
|
187
|
+
exists and what KIND it is; then it searches for each entity to confirm the
|
|
188
|
+
index can actually deliver it.
|
|
189
|
+
|
|
190
|
+
This matters more than it looks, because `required` is what BOTH retrieval
|
|
191
|
+
numbers are computed against: whether the agent asked for the entity, and
|
|
192
|
+
whether it came back. An id retrieval cannot deliver makes both fiction, and
|
|
193
|
+
the case reports a retrieval miss on every run, which reads as a defect in
|
|
194
|
+
the model or the agent rather than in the key.
|
|
195
|
+
|
|
196
|
+
**Prefer the compiled model over a grep, and prefer it in both directions.**
|
|
197
|
+
Measured on one package: `dimension:flights:flight_count` names a field the
|
|
198
|
+
model really has, so grepping the text passes it, and the compiled model says
|
|
199
|
+
`flight_count` is a MEASURE, which no `dimension` request can return.
|
|
200
|
+
Conversely `dimension:airports:own_type` appears zero times in the `.malloy`
|
|
201
|
+
and the compiled model declares it, because the source exposes it implicitly
|
|
202
|
+
from the data; the grep calls it missing and acting on that deletes a good
|
|
203
|
+
entity. `verify_goldens.py` check 5 is the grep, it is free and needs no
|
|
204
|
+
server, and it is not the authority.
|
|
205
|
+
|
|
206
|
+
A pass is a floor, not a verdict on the docs: each entity is searched by its
|
|
207
|
+
OWN NAME, the easiest query that could find it. An entity that answers to its
|
|
208
|
+
identifier and not to the words a question uses still fails at run time, as
|
|
209
|
+
`not retrieved`.
|
|
210
|
+
|
|
211
|
+
Writing the list forces the question an author has to answer anyway: what
|
|
212
|
+
would a reasonable agent search for, and of what type, to find this? If you
|
|
213
|
+
cannot state that, the agent cannot be expected to guess it.
|
|
214
|
+
- `status: provisional`, never `verified`, for the reason stated above: you
|
|
215
|
+
derived it THROUGH the model under test, so a model bug would certify its
|
|
216
|
+
own key. `verify_goldens.py --promote` is still the only way out.
|
|
217
|
+
- **Where two readings are both defensible from the model, do not pick one.**
|
|
218
|
+
A model that documents two conventions for the same population produces two
|
|
219
|
+
honest numbers, and choosing quietly is how a confident wrong key gets
|
|
220
|
+
written. Go to Hold an ambiguous golden in `skill:eval-loop`'s
|
|
221
|
+
`reference/golden-side-door.md`: record the competing candidates rather than
|
|
222
|
+
a new number.
|
|
223
|
+
- What no query can settle is not a failure to derive. It is step 4's second
|
|
224
|
+
kind, and those clauses score on day one.
|
|
225
|
+
|
|
98
226
|
## Step 3: run their query, if they gave one
|
|
99
227
|
|
|
100
228
|
Do it at import, before any run. It is the cheapest finding in the whole loop.
|
|
@@ -151,6 +279,57 @@ Where you cannot decide, mark the case and report it rather than guessing. A
|
|
|
151
279
|
criterion nobody could classify is a question for its author, and
|
|
152
280
|
`skill:eval-loop`'s golden side door is where it waits.
|
|
153
281
|
|
|
282
|
+
### Four rules for writing the rubric itself
|
|
283
|
+
|
|
284
|
+
Each of these cost a scored case on a real set, and none of them is obvious
|
|
285
|
+
while you are writing one.
|
|
286
|
+
|
|
287
|
+
**The golden rows are the figures. The rubric's prose is a gloss on them, and
|
|
288
|
+
where the two disagree the prose is what is stale.** On one set 26 of 29
|
|
289
|
+
rubrics quoted a figure that appears nowhere in their own golden rows: the
|
|
290
|
+
goldens were re-derived the next morning, the prose was not, and an agent that
|
|
291
|
+
computed 747 and 370 -- the exact numbers in the golden JSON -- was failed
|
|
292
|
+
against a rubric still saying 615 and 502. Say how to derive the figure, not
|
|
293
|
+
what it equalled. `verify_goldens.py` check 2 reports figures in the accepting
|
|
294
|
+
clause that are absent from the rows, but it reads only figures specific enough
|
|
295
|
+
to be a quoted result -- a bare three-digit number is invisible to it, and 615
|
|
296
|
+
is exactly that. So the check is a help, not a guarantee, and the rule above is
|
|
297
|
+
yours to keep. The judge is told the same thing from the other side: where a
|
|
298
|
+
rubric and a golden disagree about a figure, it scores against the golden.
|
|
299
|
+
|
|
300
|
+
**Do not assert the model's current behaviour.** "`contract_terms` cannot be
|
|
301
|
+
used here at all -- it returns ZERO rows" was true when written and false four
|
|
302
|
+
hours later, when a commit unblocked that view. Nothing linked the two, and the
|
|
303
|
+
judge then reasoned from the stale claim against an answer that was right.
|
|
304
|
+
Co-locating the set with the model makes such drift visible in a diff; it does
|
|
305
|
+
not detect it. A rubric that describes a bug is a rubric with an expiry date:
|
|
306
|
+
write the requirement, not the defect.
|
|
307
|
+
|
|
308
|
+
**When a question admits two honest populations, accept either and require the
|
|
309
|
+
answer to name which.** "Products in a category" can mean listed-in or
|
|
310
|
+
primary-category, and on one set the two readings differed by 4,070 against
|
|
311
|
+
2,707. Six or more cases turned on it and one rubric had been written to accept
|
|
312
|
+
both. That one was right. The rest were repaired afterwards, having failed
|
|
313
|
+
correct answers in the meantime.
|
|
314
|
+
|
|
315
|
+
**If the rubric accepts an alternative, the entity list must too.** The two
|
|
316
|
+
halves of a key are read by different things: the rubric is prose for the judge,
|
|
317
|
+
`expectedEntities.required` is ids for retrieval scoring. They can disagree
|
|
318
|
+
without anything noticing. A rubric saying "either the full carrier name or the
|
|
319
|
+
nickname is fine" beside a required list naming only `dimension:carriers:name`
|
|
320
|
+
scores an answer that used the nickname CORRECT and docks it recall in the same
|
|
321
|
+
run, and that lost recall then reads as a retrieval failure. Use a
|
|
322
|
+
`requiredAnyOf` group, which is satisfied when any member is delivered.
|
|
323
|
+
`verify_goldens.py` reports the mismatch as a review item, but the rule is
|
|
324
|
+
yours: it is a heuristic over prose and cannot catch every phrasing.
|
|
325
|
+
|
|
326
|
+
**A trap note is not a requirement.** Notes that arrive beside the questions
|
|
327
|
+
describe what their author thought was hard, and they mention things the
|
|
328
|
+
question never asked for. Turning one into a rubric clause invents a
|
|
329
|
+
requirement the answerer was never given, and it happened: a term appearing
|
|
330
|
+
nowhere in any question became a clause an answer was marked down for missing.
|
|
331
|
+
Convert a note only where it constrains the answer to the question as asked.
|
|
332
|
+
|
|
154
333
|
## Step 5: never change a question, and seal it
|
|
155
334
|
|
|
156
335
|
The question is the stimulus. It is what a human asked, and it is not yours to
|
|
@@ -18,7 +18,7 @@ On the case:
|
|
|
18
18
|
| `split` | rarely present | you choose, and freeze it at import |
|
|
19
19
|
| `source` | how it arrived: the filename, or the log query | say `unknown` rather than guessing a provenance |
|
|
20
20
|
| `golden` | see the classification table in `SKILL.md` | absent. A case with no golden is legitimate |
|
|
21
|
-
| `expectedEntities` | almost never present | leave it out. Guessing which fields an answer needs invents a retrieval expectation nobody stated, and it scores as a retrieval miss forever |
|
|
21
|
+
| `expectedEntities` | almost never present | leave it out. Guessing which fields an answer needs invents a retrieval expectation nobody stated, and it scores as a retrieval miss forever. The one exception is a key you derived yourself (`SKILL.md`, Deriving a key yourself): then you are not guessing, you are recording what you used |
|
|
22
22
|
|
|
23
23
|
`expectedEntities` is the one most worth leaving empty. A wrong `required` id
|
|
24
24
|
cannot be delivered by any run, so it reads as a model failure on every case
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: eval-improve
|
|
3
3
|
description: 'Make the smallest safe Malloy model edit that closes a diagnosed model-owned gap, with a probe receipt for every factual claim. Use after eval-diagnose, or when asked to fix a model so an agent can discover the right answer. Never accepts its own edit; the acceptance check belongs to eval-loop. Does not decide whether an answer was wrong (eval-answer) or why (eval-diagnose).'
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.21
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Improve the Model
|