@malloy-publisher/skills 0.1.21 → 0.1.23
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 +1 -1
- package/skills/eval-answer/scripts/check_findable.py +99 -0
- package/skills/eval-answer/scripts/check_findable_test.py +38 -0
- package/skills/eval-answer/scripts/score_retrieval.py +14 -1
- package/skills/eval-answer/scripts/score_retrieval_test.py +8 -1
- package/skills/eval-answer/scripts/verify_goldens.py +44 -0
- package/skills/eval-diagnose/SKILL.md +16 -1
- package/skills/eval-import/SKILL.md +1 -1
- package/skills/eval-improve/SKILL.md +1 -1
- package/skills/eval-judge/SKILL.md +22 -1
- package/skills/eval-loop/SKILL.md +21 -6
- package/skills/eval-loop/scripts/build_run_package.py +17 -4
- package/skills/eval-loop/scripts/run_baseline.py +117 -6
- package/skills/eval-loop/scripts/run_baseline_test.py +127 -12
- package/skills/eval-loop/templates/eval-run-package/public/app.js +29 -4
- package/skills/eval-loop/templates/eval-run-package/public/index.html +35 -7
- package/skills/eval-report/SKILL.md +101 -12
- package/skills/malloy/SKILL.md +2 -1
- package/skills/malloy-analysis/SKILL.md +2 -2
- package/skills/malloy-analysis-pitfalls/SKILL.md +2 -2
- 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 +26 -8
- 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 +43 -24
- 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 +2 -2
- package/skills/malloy-html-data-apps/SKILL.md +1 -1
- package/skills/malloy-lookml-review/SKILL.md +1 -1
- package/skills/malloy-lookml-review/reference/curate-visibility.md +1 -1
- package/skills/malloy-materialization/SKILL.md +3 -3
- package/skills/malloy-materialization-tuning/SKILL.md +2 -2
- package/skills/malloy-model/SKILL.md +51 -17
- package/skills/malloy-model-as-you-go/SKILL.md +2 -2
- package/skills/malloy-modeling/SKILL.md +2 -2
- package/skills/malloy-notebook-chat/SKILL.md +1 -1
- package/skills/malloy-notebooks/SKILL.md +2 -2
- package/skills/malloy-patterns/SKILL.md +1 -1
- package/skills/malloy-phrase-detection/SKILL.md +14 -9
- package/skills/malloy-publish/SKILL.md +31 -12
- package/skills/malloy-queries/SKILL.md +11 -12
- package/skills/malloy-review/SKILL.md +1 -1
- package/skills/malloy-scope/SKILL.md +1 -1
- package/skills/malloy-source-unreachable/SKILL.md +71 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: eval-answer
|
|
3
3
|
description: 'Score one analytical answer against a verified golden, and score which of the entities the golden depends on retrieval delivered to the answerer. Run the contamination checklist, re-execute the submitted query yourself, then spawn a judge subagent per skill:eval-judge. Append attempt, tool_call, score, and retrieval_score events to the file ledger (reference/ledger-schema.md). Never explain the failure (eval-diagnose) or edit the model (eval-improve). Use when asked whether an answer was correct, to score a run, or to baseline a model.'
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.23
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Evaluate One Answer
|
|
@@ -51,6 +51,7 @@ import argparse
|
|
|
51
51
|
import json
|
|
52
52
|
import pathlib
|
|
53
53
|
import sys
|
|
54
|
+
import time
|
|
54
55
|
import urllib.error
|
|
55
56
|
import urllib.parse
|
|
56
57
|
import urllib.request
|
|
@@ -176,6 +177,30 @@ def compiled_entities(rest_url: str, environment: str,
|
|
|
176
177
|
return out or None
|
|
177
178
|
|
|
178
179
|
|
|
180
|
+
def embedding_index_status(rest_url: str, environment: str, package: str,
|
|
181
|
+
timeout: int = 15) -> str | None:
|
|
182
|
+
"""`embeddingIndex.status` for one package, or None if it cannot be read.
|
|
183
|
+
|
|
184
|
+
Retrieval is semantic only once this reaches `ready`; before that the
|
|
185
|
+
server answers the same calls from the lexical matcher without saying so,
|
|
186
|
+
and fewer entities come back. A checker that does not wait for it reports
|
|
187
|
+
the difference as a broken answer key.
|
|
188
|
+
"""
|
|
189
|
+
url = (f"{rest_url.rstrip('/')}/api/v0/environments/"
|
|
190
|
+
f"{urllib.parse.quote(environment)}/packages/"
|
|
191
|
+
f"{urllib.parse.quote(package)}")
|
|
192
|
+
try:
|
|
193
|
+
with urllib.request.urlopen(url, timeout=timeout) as r:
|
|
194
|
+
body = json.loads(r.read())
|
|
195
|
+
except (urllib.error.URLError, OSError, ValueError):
|
|
196
|
+
return None
|
|
197
|
+
index = body.get("embeddingIndex")
|
|
198
|
+
if not isinstance(index, dict):
|
|
199
|
+
return None
|
|
200
|
+
status = index.get("status")
|
|
201
|
+
return status if isinstance(status, str) else None
|
|
202
|
+
|
|
203
|
+
|
|
179
204
|
def stale_packages(rest_url: str, timeout: int = 30) -> set[tuple[str, str]] | None:
|
|
180
205
|
"""`{(environment, package)}` serving a model older than their files.
|
|
181
206
|
|
|
@@ -214,6 +239,26 @@ def declared_findings(cases: list[dict[str, Any]],
|
|
|
214
239
|
continue
|
|
215
240
|
if f"{kind}:{name}" in declared[src]:
|
|
216
241
|
continue
|
|
242
|
+
# A DOTTED name is a join path, not a field name. `declared[src]` holds
|
|
243
|
+
# the source's OWN fields, so `dimension:order_items:products.brand`
|
|
244
|
+
# never matched and every dotted id was reported as a missing field --
|
|
245
|
+
# five of them on one set, against a get_context that returns each as
|
|
246
|
+
# its top result. Resolve it where it lives: the last segment is the
|
|
247
|
+
# field, the segment before it names the joined source.
|
|
248
|
+
if "." in name:
|
|
249
|
+
*path, leaf = name.split(".")
|
|
250
|
+
hop = path[-1]
|
|
251
|
+
if hop in declared and f"{kind}:{leaf}" in declared[hop]:
|
|
252
|
+
continue
|
|
253
|
+
if hop not in declared:
|
|
254
|
+
out.append(f"{eid}: the join path names {hop!r}, which the "
|
|
255
|
+
f"compiled model has no source for "
|
|
256
|
+
f"(required by {', '.join(qids)})")
|
|
257
|
+
else:
|
|
258
|
+
out.append(f"{eid}: the compiled {hop} source declares no "
|
|
259
|
+
f"{kind} {leaf!r} to reach through that join "
|
|
260
|
+
f"(required by {', '.join(qids)})")
|
|
261
|
+
continue
|
|
217
262
|
other = sorted(k.split(":", 1)[0] for k in declared[src]
|
|
218
263
|
if k.split(":", 1)[1] == name)
|
|
219
264
|
if other:
|
|
@@ -310,6 +355,11 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
310
355
|
"both directions.")
|
|
311
356
|
ap.add_argument("--out", default=None,
|
|
312
357
|
help="write the per-entity rows as JSON")
|
|
358
|
+
ap.add_argument("--index-wait", type=int, default=300,
|
|
359
|
+
help="seconds to wait for the embedding index to be ready "
|
|
360
|
+
"before searching (needs --publisher). 0 to skip the "
|
|
361
|
+
"wait, which risks reading the lexical matcher as a "
|
|
362
|
+
"missing entity")
|
|
313
363
|
a = ap.parse_args(argv)
|
|
314
364
|
|
|
315
365
|
f = a.set_dir / "cases.jsonl"
|
|
@@ -318,6 +368,55 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
318
368
|
return 3
|
|
319
369
|
cases = [json.loads(line) for line in f.read_text().splitlines() if line.strip()]
|
|
320
370
|
|
|
371
|
+
# The embedding index is built lazily, on the first call that ranks. A
|
|
372
|
+
# search run seconds after a server start is answered by the LEXICAL
|
|
373
|
+
# matcher, which finds fewer things -- and this check reads "fewer things"
|
|
374
|
+
# as "the key names entities that cannot be retrieved" and tells you to fix
|
|
375
|
+
# the key. Three ids were reported missing that way on one set and all
|
|
376
|
+
# three passed on a re-run. So wait for it, and say so when it cannot.
|
|
377
|
+
if a.publisher and a.index_wait:
|
|
378
|
+
# The server's enum is indexing | ready | cooldown | too-many-entities.
|
|
379
|
+
# `cooldown` (the embedding provider failed recently) and
|
|
380
|
+
# `too-many-entities` (over the cap, permanently lexical) will never
|
|
381
|
+
# become `ready`, so waiting on them burns the whole timeout for a
|
|
382
|
+
# result that is knowable on the first read.
|
|
383
|
+
TERMINAL = ("ready", "cooldown", "too-many-entities")
|
|
384
|
+
# `indexing` does NOT clear on its own: the docs say it clears "once a
|
|
385
|
+
# get_context question triggers a sync, which is what builds the
|
|
386
|
+
# index". Polling the package resource never triggers one, so a wait
|
|
387
|
+
# that only polls is a guaranteed stall on a freshly loaded package.
|
|
388
|
+
# One ranking call first, as serve.py --warm-retrieval does.
|
|
389
|
+
try:
|
|
390
|
+
get_context(a.mcp_url, [{"target_type": "source"}],
|
|
391
|
+
a.environment, a.package)
|
|
392
|
+
except (urllib.error.URLError, RuntimeError, OSError):
|
|
393
|
+
pass # the search below reports it properly
|
|
394
|
+
deadline = time.monotonic() + a.index_wait
|
|
395
|
+
status = embedding_index_status(a.publisher, a.environment, a.package)
|
|
396
|
+
while status not in TERMINAL and status is not None \
|
|
397
|
+
and time.monotonic() < deadline:
|
|
398
|
+
time.sleep(2)
|
|
399
|
+
status = embedding_index_status(a.publisher, a.environment,
|
|
400
|
+
a.package)
|
|
401
|
+
if status is None:
|
|
402
|
+
print("! the embedding index status could not be read; a miss "
|
|
403
|
+
"below may be the lexical matcher rather than the key",
|
|
404
|
+
file=sys.stderr)
|
|
405
|
+
elif status == "too-many-entities":
|
|
406
|
+
print("! this package is over the embedding cap, so retrieval is "
|
|
407
|
+
"permanently lexical here. The misses below are real for "
|
|
408
|
+
"this server and say nothing about a semantic one",
|
|
409
|
+
file=sys.stderr)
|
|
410
|
+
elif status == "cooldown":
|
|
411
|
+
print("! the embedding provider is in cooldown, so retrieval is "
|
|
412
|
+
"lexical for now. Re-run rather than acting on a miss below",
|
|
413
|
+
file=sys.stderr)
|
|
414
|
+
elif status != "ready":
|
|
415
|
+
print(f"! the embedding index is {status!r} after "
|
|
416
|
+
f"{a.index_wait}s, so this search may be answered "
|
|
417
|
+
f"lexically; a miss below may be the index, not the key",
|
|
418
|
+
file=sys.stderr)
|
|
419
|
+
|
|
321
420
|
declared_out: list[str] = []
|
|
322
421
|
if a.publisher:
|
|
323
422
|
# Staleness first. A stale package answers the models endpoint normally
|
|
@@ -322,5 +322,43 @@ class Staleness(unittest.TestCase):
|
|
|
322
322
|
self.assertIsNone(check_findable.stale_packages("http://127.0.0.1:9"))
|
|
323
323
|
|
|
324
324
|
|
|
325
|
+
class DottedJoinPaths(unittest.TestCase):
|
|
326
|
+
"""A dotted name is a join path, resolved on the joined source.
|
|
327
|
+
|
|
328
|
+
`declared[src]` holds a source's OWN fields, so every dotted id read as a
|
|
329
|
+
field the source does not declare. On the storefront tour set that was five
|
|
330
|
+
of eight reported misses, each one an id get_context returns as its top
|
|
331
|
+
result -- and the check ends by telling you to fix the key, so a false miss
|
|
332
|
+
here sends you to break an answer key that is correct.
|
|
333
|
+
"""
|
|
334
|
+
|
|
335
|
+
DECLARED = {
|
|
336
|
+
"order_items": {"source:order_items", "measure:total_sales"},
|
|
337
|
+
"products": {"source:products", "dimension:brand", "measure:product_count"},
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
def findings(self, eid):
|
|
341
|
+
cases = [{"qid": "q1", "expectedEntities": {"required": [eid]}}]
|
|
342
|
+
return check_findable.declared_findings(cases, self.DECLARED)
|
|
343
|
+
|
|
344
|
+
def test_a_reachable_join_path_is_not_a_finding(self):
|
|
345
|
+
self.assertEqual(self.findings("dimension:order_items:products.brand"), [])
|
|
346
|
+
|
|
347
|
+
def test_the_kind_still_has_to_match_on_the_joined_source(self):
|
|
348
|
+
out = self.findings("measure:order_items:products.brand")
|
|
349
|
+
self.assertEqual(len(out), 1)
|
|
350
|
+
self.assertIn("declares no measure 'brand'", out[0])
|
|
351
|
+
|
|
352
|
+
def test_an_unknown_hop_is_named_as_the_hop(self):
|
|
353
|
+
out = self.findings("dimension:order_items:suppliers.name")
|
|
354
|
+
self.assertEqual(len(out), 1)
|
|
355
|
+
self.assertIn("'suppliers'", out[0])
|
|
356
|
+
|
|
357
|
+
def test_a_field_missing_on_the_joined_source_is_a_finding(self):
|
|
358
|
+
out = self.findings("dimension:order_items:products.colour")
|
|
359
|
+
self.assertEqual(len(out), 1)
|
|
360
|
+
self.assertIn("'colour'", out[0])
|
|
361
|
+
|
|
362
|
+
|
|
325
363
|
if __name__ == "__main__":
|
|
326
364
|
unittest.main()
|
|
@@ -528,7 +528,17 @@ def cascade(rows: list[dict[str, Any]]) -> dict[str, int]:
|
|
|
528
528
|
# pins that identity never caught it because every row it builds
|
|
529
529
|
# supplies a required entity.
|
|
530
530
|
"passed_not_covered": 0, "passed_not_retrieved": 0,
|
|
531
|
-
"passed_unmeasured": 0, "passed_no_entities_named": 0
|
|
531
|
+
"passed_unmeasured": 0, "passed_no_entities_named": 0,
|
|
532
|
+
# Recall, tallied OUTSIDE the funnel. The funnel is an elif chain, so a
|
|
533
|
+
# row whose coverage was never measured stops at rung 1 and never
|
|
534
|
+
# reaches the recall check -- `not retrieved` is then structurally 0,
|
|
535
|
+
# and a display that subtracts it from a denominator reports every
|
|
536
|
+
# retrieval as a success. That is what happened: 12 cases, coverage
|
|
537
|
+
# unknown, three of them at recall 0.5, printed "retrieved? 12 yes, 0
|
|
538
|
+
# no". These two count every row whose recall was actually computed and
|
|
539
|
+
# whose coverage is not a KNOWN gap, so the rung says what was measured
|
|
540
|
+
# whether or not coverage ran.
|
|
541
|
+
"recall_scored": 0, "recall_short": 0}
|
|
532
542
|
for r in rows:
|
|
533
543
|
cov = r["coverage"]
|
|
534
544
|
passed = not r["failed"] and r["verdict"] not in UNSCORED
|
|
@@ -550,6 +560,9 @@ def cascade(rows: list[dict[str, Any]]) -> dict[str, int]:
|
|
|
550
560
|
c["delivered, wrong"] += 1
|
|
551
561
|
else:
|
|
552
562
|
c["delivered, right"] += 1
|
|
563
|
+
if cov not in MEASURED_GAPS and r["recall"] is not None:
|
|
564
|
+
c["recall_scored"] += 1
|
|
565
|
+
c["recall_short"] += r["recall"] < 1.0
|
|
553
566
|
return c
|
|
554
567
|
|
|
555
568
|
|
|
@@ -437,8 +437,15 @@ class Cascade(unittest.TestCase):
|
|
|
437
437
|
# `passed_not_covered` and `passed_not_retrieved` OVERLAY the rungs: they
|
|
438
438
|
# re-count rows already counted by "not covered" and "not retrieved". They
|
|
439
439
|
# are not rungs and must never be added to them.
|
|
440
|
+
#
|
|
441
|
+
# `recall_scored` / `recall_short` are a parallel TALLY, not a rung either.
|
|
442
|
+
# The rungs are an elif chain, so a row whose coverage was never measured
|
|
443
|
+
# stops at the first one and its recall is never classified; these two
|
|
444
|
+
# count recall over every row that carried one, which is what the retrieval
|
|
445
|
+
# line reports. Same rows, counted on a second axis -- so they overlay.
|
|
440
446
|
OVERLAY = ("total", "passed_not_covered", "passed_not_retrieved",
|
|
441
|
-
"passed_unmeasured", "passed_no_entities_named"
|
|
447
|
+
"passed_unmeasured", "passed_no_entities_named",
|
|
448
|
+
"recall_scored", "recall_short")
|
|
442
449
|
|
|
443
450
|
def test_every_row_lands_on_exactly_one_rung(self):
|
|
444
451
|
c = cascade(self.rows())
|
|
@@ -518,6 +518,49 @@ def promotion_blocker(case: dict[str, Any], set_dir: pathlib.Path) -> str | None
|
|
|
518
518
|
return None
|
|
519
519
|
|
|
520
520
|
|
|
521
|
+
# A figure is a number a reader would check: 8,817.56, 943, 25%. Years and
|
|
522
|
+
# small ordinals are not -- "top 3", "2025" -- so they do not raise a finding.
|
|
523
|
+
FIGURE = re.compile(r"(?<![\w.])(\d{1,3}(?:,\d{3})+(?:\.\d+)?|\d+\.\d+|\d{3,})")
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def criteria_figure_findings(case: dict[str, Any]) -> list[str]:
|
|
527
|
+
"""Numbers quoted inside a `criteria` golden, which nothing can verify.
|
|
528
|
+
|
|
529
|
+
A criteria golden is exempt from both numeric checks and legitimately so:
|
|
530
|
+
it holds no value to re-derive, and the rubric-figure check compares
|
|
531
|
+
figures against the golden's ROWS, which it does not have. The clauses are
|
|
532
|
+
prose and prose cannot drift.
|
|
533
|
+
|
|
534
|
+
Its prose can still assert numbers, and then the exemption is a hole rather
|
|
535
|
+
than a simplification. This set shipped one: a criteria golden's note said
|
|
536
|
+
"by order count it is Amelia Cohen (80 orders)". Both halves were wrong --
|
|
537
|
+
at the grain the same rubric requires, the top by order count is the same
|
|
538
|
+
customer who tops spend, and 80 is three customers sharing a name, summed
|
|
539
|
+
by exactly the grouping the neighbouring case exists to forbid. Every check
|
|
540
|
+
passed it, because every check skips this kind of golden.
|
|
541
|
+
|
|
542
|
+
Reported as review, not failure: quoting a figure is often right, and the
|
|
543
|
+
point is that a human has to be the one who checked it.
|
|
544
|
+
"""
|
|
545
|
+
g = case.get("golden") or {}
|
|
546
|
+
if g.get("kind") != "criteria":
|
|
547
|
+
return []
|
|
548
|
+
out = []
|
|
549
|
+
for field, text in (("rubric", g.get("rubric")),
|
|
550
|
+
("verification.note", (g.get("verification") or {}).get("note"))):
|
|
551
|
+
if not isinstance(text, str):
|
|
552
|
+
continue
|
|
553
|
+
figs = sorted(set(FIGURE.findall(text)))
|
|
554
|
+
if figs:
|
|
555
|
+
out.append(f"review {case['qid']}: the criteria golden's {field} quotes "
|
|
556
|
+
f"{', '.join(figs[:6])}"
|
|
557
|
+
f"{' and more' if len(figs) > 6 else ''}, and NOTHING in this "
|
|
558
|
+
f"toolchain can check a figure in a criteria golden -- it has no "
|
|
559
|
+
f"rows for the rubric-figure check and no value to re-derive. "
|
|
560
|
+
f"Query each one at the grain this rubric requires, or drop it")
|
|
561
|
+
return out
|
|
562
|
+
|
|
563
|
+
|
|
521
564
|
def axis_findings(case: dict[str, Any], set_dir: pathlib.Path) -> list[str]:
|
|
522
565
|
g = case.get("golden") or {}
|
|
523
566
|
if g.get("kind") == "unanswerable":
|
|
@@ -1021,6 +1064,7 @@ def verify(set_dir: pathlib.Path, publisher: str, environment: str,
|
|
|
1021
1064
|
for c in chosen:
|
|
1022
1065
|
findings += rubric_number_findings(c)
|
|
1023
1066
|
findings += axis_findings(c, set_dir)
|
|
1067
|
+
findings += criteria_figure_findings(c)
|
|
1024
1068
|
findings += rubric_alternative_findings(chosen)
|
|
1025
1069
|
findings += stale_rubric_claims(chosen, model_definitions(model))
|
|
1026
1070
|
findings += unknown_name_findings(chosen, model_text(model))
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: eval-diagnose
|
|
3
3
|
description: 'Diagnose why a scored answer failed and who owns the fix, then cluster the failures by shared root cause. Walk dataset, agent-call, get_context/model, get_context/retrieval, construction, then model-definition. Append issue events to the file ledger, linked by traceId, one per cluster. Use after eval-answer, when triaging a run, or before changing a model. Does not edit the model (eval-improve).'
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.23
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Diagnose One Answer
|
|
@@ -221,6 +221,21 @@ If the agent could not reasonably have known to ask, that is a model gap.
|
|
|
221
221
|
| `GUIDANCE-NOT-RETRIEVED` | entities came back, governing guidance did not | model: put guidance on the entities agents search for |
|
|
222
222
|
| `GUIDANCE-DECLINED` | guidance was retrieved and judged inapplicable | model: state the business default, not a caveat |
|
|
223
223
|
|
|
224
|
+
**Before diagnosing a `NOT-RETURNED`, check the call actually returned
|
|
225
|
+
nothing.** The `tool_call` event carries `retrieval_mode` and `rankedSummary`.
|
|
226
|
+
A `rankedSummary` of `null` means the response could not be read, not that it
|
|
227
|
+
was empty -- a body too large for the model's context is written to a file, and
|
|
228
|
+
the run records no summary for it. There is nothing to diagnose there: say the
|
|
229
|
+
call was not measured.
|
|
230
|
+
|
|
231
|
+
And **never attribute a miss to the embedding index unless `retrieval_mode`
|
|
232
|
+
says `lexical`.** The mode is recorded per call precisely so this is checkable.
|
|
233
|
+
A diagnosis once explained a phantom empty result with "the index was not ready
|
|
234
|
+
yet" on a run whose index was ready before the first question and whose
|
|
235
|
+
response did hold results; the empty list was a parsing bug in the harness. An
|
|
236
|
+
unverifiable cause that sounds right is worse than `needs_human`, because it
|
|
237
|
+
closes the finding.
|
|
238
|
+
|
|
224
239
|
A missing join is coverage, not an agent-call miss. The model has to volunteer
|
|
225
240
|
relationships. A declared join is not a retrieval entity; do not look for it in
|
|
226
241
|
`get_context` results.
|
|
@@ -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.23
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Import questions into an eval set
|
|
@@ -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.23
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Improve the Model
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: eval-judge
|
|
3
3
|
description: 'Decide whether ONE answer matches its golden, and say whether you believe the golden. Read this before emitting any verdict. Covers containment, column pairing, near_match, refusals, and the gold_status judgement. Use when scoring an attempt in an evaluation run; never to conduct a run (eval-loop), diagnose a failure (eval-diagnose) or edit a model (eval-improve).'
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.23
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# The judge
|
|
@@ -119,6 +119,27 @@ Output, exactly this shape:
|
|
|
119
119
|
reading that produced it, that is `no_match`. Use `near_match` only when you
|
|
120
120
|
can name the rubric clause that makes the difference defensible.
|
|
121
121
|
|
|
122
|
+
**A rubric clause cannot make a wrong VALUE defensible, and a clause that
|
|
123
|
+
tries is a defect in the rubric rather than a licence to you.** `near_match`
|
|
124
|
+
turns on the answer being right under a reading the QUESTION allows -- a tie
|
|
125
|
+
broken the other way, a grain the question left open, a basis the question
|
|
126
|
+
never fixed. It does not turn on the answer being transparent about how it
|
|
127
|
+
got a figure the question did not ask for. Those two look alike in a rubric
|
|
128
|
+
and are opposites in a report: one is a number a reader can act on, the
|
|
129
|
+
other is a number a reader would act on wrongly. Naming the method makes a
|
|
130
|
+
wrong figure DIAGNOSABLE, which is worth having, and it is not partial
|
|
131
|
+
credit.
|
|
132
|
+
|
|
133
|
+
The test, before you write `near_match` on a case with a value: would a
|
|
134
|
+
reader who acted on this figure be wrong? If yes, it is `no_match` however
|
|
135
|
+
plainly the answer explained itself, and however the rubric is worded. Say
|
|
136
|
+
in `why` that you are overriding a rubric clause, so the clause gets fixed.
|
|
137
|
+
This rule exists because a set shipped one: a question asked for sales over
|
|
138
|
+
the company's season, the answer gave the meteorological window 25% lower
|
|
139
|
+
and said which window it used, and a clause granting `near_match` for a
|
|
140
|
+
stated window kept a materially wrong answer out of the pass rate
|
|
141
|
+
entirely -- the arm reported 100%.
|
|
142
|
+
|
|
122
143
|
It is a third outcome because as a pass it was a large share of the measured
|
|
123
144
|
noise: the same unchanged answer reads `match` in one run and `near_match`
|
|
124
145
|
in the next, and the pass rate moves although nothing did. A verdict whose
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: eval-loop
|
|
3
3
|
description: 'Conduct a local Publisher evaluation loop in five steps: scrape/run, eval, diagnose, improve, checkpoint. You are the conductor: import cases into the file ledger, spawn a blind answerer, then run eval-answer, eval-diagnose, and eval-improve. Persistence is plain files under the model package''s evals/ directory; checkpoints are git commits of the model repo. Use to score a model, diagnose failures, improve behind an acceptance check, or roll back a bad direction.'
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.23
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# The Evaluation Loop
|
|
@@ -328,11 +328,19 @@ the run measure something other than what it names:
|
|
|
328
328
|
first time somebody tidies up; the set, the run directory and the write-up
|
|
329
329
|
belong in git beside the model.
|
|
330
330
|
|
|
331
|
-
2. The server must be up
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
331
|
+
2. The server must be up. Where your host offers retrieval tracing, turn it on
|
|
332
|
+
and confirm a trace lookup answers, so a call's ranked results can be
|
|
333
|
+
recovered afterwards.
|
|
334
|
+
|
|
335
|
+
**Open-source Publisher has none.** `PUBLISHER_MCP_TRACE=retrieval` is set by
|
|
336
|
+
`serve.py` and read by nothing: the trace store was written and never merged,
|
|
337
|
+
so there is no trace tool and `traceId` is null on every local attempt. This
|
|
338
|
+
does not block a scored run, because attribution never depended on it:
|
|
339
|
+
`rankedSummary` is copied onto the `tool_call` event at capture, precisely so
|
|
340
|
+
the evidence survives without a store. So do not refuse a local run for want
|
|
341
|
+
of tracing -- an earlier version of this rule did, and it refused every local
|
|
342
|
+
run there has ever been. Refuse one whose `tool_call` events carry no
|
|
343
|
+
`rankedSummary`, which is what attribution actually reads.
|
|
336
344
|
|
|
337
345
|
3. Health-check: your host's status check until it reports serving, and inspect
|
|
338
346
|
`loadErrors`. A dead database that still answers HTTP is an environment
|
|
@@ -500,6 +508,13 @@ conductor. Do not:
|
|
|
500
508
|
|
|
501
509
|
- The model is the only thing improve edits. No question text, qids, or
|
|
502
510
|
expected values in any name, doc, or comment.
|
|
511
|
+
- **You are measuring a model, not reviewing this harness.** Read a script when
|
|
512
|
+
a number you have to report cannot be explained otherwise, and stop there. Do
|
|
513
|
+
not audit the scripts, propose fixes to them, or hand back tooling critique
|
|
514
|
+
in place of a result -- a run that ends in harness feedback has not answered
|
|
515
|
+
the question it was asked. A defect that changed a number gets one sentence
|
|
516
|
+
in the report; a defect that changed nothing gets one line in chat, once.
|
|
517
|
+
Fixing it is a different task, and the user starts it.
|
|
503
518
|
- When the environment misbehaves, stop. Never diagnose a sick system. The
|
|
504
519
|
harness is part of the environment: a known-broken measurement does not
|
|
505
520
|
become quotable by being finished. Measured against this directive, an arm
|
|
@@ -396,7 +396,8 @@ def build(run_dirs: list[pathlib.Path], set_dir: pathlib.Path,
|
|
|
396
396
|
retr.append({"attempt_key": ak, "run_id": run_id, **r})
|
|
397
397
|
|
|
398
398
|
exp = case.get("expectedEntities") or {}
|
|
399
|
-
|
|
399
|
+
required_groups = groups(exp)
|
|
400
|
+
required = {e for g in required_groups for e in g}
|
|
400
401
|
acceptable = set(exp.get("acceptable") or []) | required
|
|
401
402
|
got: dict[str, None] = {}
|
|
402
403
|
mine_tokens: list[str] = []
|
|
@@ -412,11 +413,23 @@ def build(run_dirs: list[pathlib.Path], set_dir: pathlib.Path,
|
|
|
412
413
|
# named in returned source docs (from the ledger when the run
|
|
413
414
|
# recorded them, else from the transcript walked above).
|
|
414
415
|
tokens = {t for t in mine_tokens} | set(_IDENT.findall(docs))
|
|
415
|
-
|
|
416
|
+
# ONE ROW PER GROUP, not per entity. `groups()` is the unit recall
|
|
417
|
+
# is scored in -- a `requiredAnyOf` group is satisfied by any one
|
|
418
|
+
# member -- and flattening it here made the app's badge disagree
|
|
419
|
+
# with the recall metric printed beside it: a case with one
|
|
420
|
+
# `required` plus a three-way alternative rendered four dots and
|
|
421
|
+
# read `2/4` in red on a case whose recall was 2 of 2. Nine of the
|
|
422
|
+
# storefront set's twelve cases carry such a group.
|
|
423
|
+
RANK = {"exact": 0, "alias": 1, "in_docs": 2, "missing": 3}
|
|
424
|
+
for g in sorted(required_groups, key=lambda g: sorted(g)[0]):
|
|
425
|
+
best = min(((delivery(e, set(got), tokens), e) for e in g),
|
|
426
|
+
key=lambda p: RANK.get(p[0], 3))
|
|
427
|
+
status, eid = best
|
|
416
428
|
required_rows.append({
|
|
417
429
|
"attempt_key": ak, "run_id": run_id, "qid": qid,
|
|
418
|
-
"entity_id": eid
|
|
419
|
-
|
|
430
|
+
"entity_id": eid if len(g) == 1 else
|
|
431
|
+
f"{eid} (any of {len(g)})",
|
|
432
|
+
"status": status})
|
|
420
433
|
|
|
421
434
|
roles: list[tuple[str, str]] = []
|
|
422
435
|
roles += [(eid, "required") for eid in sorted(required)]
|