@malloy-publisher/skills 0.1.22 → 0.1.24
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/reference/ledger-schema.md +1 -0
- 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/init_truth_package.py +10 -0
- package/skills/eval-answer/scripts/ledger.py +1 -1
- 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 +69 -0
- package/skills/eval-answer/scripts/verify_goldens_test.py +64 -0
- package/skills/eval-diagnose/SKILL.md +16 -1
- package/skills/eval-import/SKILL.md +1 -1
- package/skills/eval-import/reference/case-format.md +1 -1
- package/skills/eval-improve/SKILL.md +1 -1
- package/skills/eval-judge/SKILL.md +46 -2
- package/skills/eval-judge/reference/writing-rubrics.md +8 -0
- package/skills/eval-loop/SKILL.md +21 -6
- package/skills/eval-loop/reference/running-a-run.md +4 -0
- package/skills/eval-loop/scripts/agent_harness.py +5 -0
- package/skills/eval-loop/scripts/agent_harness_test.py +3 -1
- package/skills/eval-loop/scripts/build_run_package.py +17 -4
- package/skills/eval-loop/scripts/run_baseline.py +192 -9
- package/skills/eval-loop/scripts/run_baseline_test.py +219 -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 +1 -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 +31 -10
- 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 +3 -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 +9 -7
- package/skills/malloy-materialization-tuning/SKILL.md +3 -2
- package/skills/malloy-model/SKILL.md +1 -1
- package/skills/malloy-model-as-you-go/SKILL.md +2 -2
- package/skills/malloy-modeling/SKILL.md +3 -3
- 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 +14 -9
- package/skills/malloy-publish/SKILL.md +31 -12
- package/skills/malloy-queries/SKILL.md +1 -1
- 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.24
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Evaluate One Answer
|
|
@@ -124,6 +124,7 @@ on that axis.
|
|
|
124
124
|
| `diagnoserModel` / `improverModel` | Written by `diagnose.py` / `improve.py` when those stages run, so the run names every LLM that touched it. Absent on a run that was only answered and judged. |
|
|
125
125
|
| `modelGitSha` | Commit of the model repo, from `--model-repo`; `-dirty` suffix when that path had uncommitted changes -- fine for a band measurement, not for an A/B pin. **Null unless `--model-repo` names the repo.** It cannot be inferred: Publisher serves a COPY under `publisher_data/`, so the tree around the served file is the server's storage, not the model's history, and the field named that until 2026-09-03. `modelSha` above is the pin that always works; this is the pointer to where those bytes are versioned, and absent beats wrong. |
|
|
126
126
|
| `modelRepo` | The path `--model-repo` named, so a reader can tell an absent pin from an unrecorded one. |
|
|
127
|
+
| `modelDir` | The package directory `--model-dir` named, when it was: the path the `-dirty` marker on `modelGitSha` was decided over. Absent means the whole repo, where any unrelated untracked file marks the model dirty. |
|
|
127
128
|
| `environment` / `package` / `modelPath` | What was served, and from where. On a platform target `environment` is the organization and `package` is the workspace the MCP URL is scoped to. |
|
|
128
129
|
| `scope` | Platform target only: the `environment/package[@version]` the answerer was told to pass as an explicit `scopes` entry on every `get_context` / `execute_query` call. A workspace can serve many packages (a personal workspace serves every package the user can read), so without this the run also measures whether retrieval picks the right package -- a different measurement. Absent means unscoped. |
|
|
129
130
|
| `targetVersion` *(pin)* | Platform target only: the published version the answers came from. Both hosted tools default an omitted version to the PINNED one, meaning whatever the workspace serves at the moment of the call, so this only describes the run when `--scope` carries it to every call. `--target-version` supplies it when the scope omits it; a scope naming a different version is refused rather than treated as an override, because a run cannot say which build answered if two pins disagree. |
|
|
@@ -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()
|
|
@@ -23,6 +23,13 @@ WHAT IT PRODUCES
|
|
|
23
23
|
the header comment says to; the script will not guess
|
|
24
24
|
semantics, because guessed semantics are exactly what a
|
|
25
25
|
truth package exists to exclude.
|
|
26
|
+
A key that composes two tables (the top N by one
|
|
27
|
+
measure, then another measure over those rows) is written
|
|
28
|
+
as a raw per-key rollup declared in truth.malloy --
|
|
29
|
+
`source: t_x_by_key is t_x -> { group_by: key; aggregate:
|
|
30
|
+
... }` -- joined from an `extend` in the canonical query.
|
|
31
|
+
A raw aggregate by key is not semantics, and the
|
|
32
|
+
Publisher refuses a join declared inline in a query.
|
|
26
33
|
publisher.json the truth package, named, with the standard description.
|
|
27
34
|
publisher.config.json (--publisher-config) a Publisher server config
|
|
28
35
|
serving ONLY the truth package, for the second server the
|
|
@@ -133,6 +140,9 @@ def main() -> int:
|
|
|
133
140
|
"// own doc for where each comes from",
|
|
134
141
|
"// - raw-column dimensions for anything nested (a VARIANT column, a JSON",
|
|
135
142
|
"// path) that a golden query will need to group by",
|
|
143
|
+
"// - a raw per-key rollup (`source: t_x_by_key is t_x -> { group_by: key;",
|
|
144
|
+
"// aggregate: ... }`) wherever a key composes two tables, joined from an",
|
|
145
|
+
"// `extend` in the canonical query: the server refuses an inline join",
|
|
136
146
|
"// Keep column names in the warehouse's own case so a golden query reads as",
|
|
137
147
|
"// a warehouse query, not as a model query.",
|
|
138
148
|
"",
|
|
@@ -169,7 +169,7 @@ RUN_REQUIRED = {"runId", "target", "answererModel", "phase", "started"}
|
|
|
169
169
|
RUN_RECOMMENDED = {"judgeModel", "judgeVersion", "datasetVersion", "modelSha",
|
|
170
170
|
"skillsVersion", "answererManifest"}
|
|
171
171
|
RUN_OPTIONAL = {"label", "effort", "environment", "package", "modelPath",
|
|
172
|
-
"modelGitSha", "mcpUrl", "publisher", "predictionsReExecuted",
|
|
172
|
+
"modelGitSha", "modelDir", "mcpUrl", "publisher", "predictionsReExecuted",
|
|
173
173
|
"serverVersion", "diagnoserModel", "improverModel",
|
|
174
174
|
"rubricSha",
|
|
175
175
|
# The prompt the judge was SHOWN, hashed. `rubricSha` covers
|
|
@@ -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())
|
|
@@ -218,6 +218,16 @@ def check_value(case: dict[str, Any], a: argparse.Namespace
|
|
|
218
218
|
# canonicalQuery" and count as an error, so a set with one criteria
|
|
219
219
|
# golden failed its own audit with exit 1.
|
|
220
220
|
return "skipped", "criteria: the clauses are the key, nothing to re-derive", None
|
|
221
|
+
if g.get("status") in ("ambiguous", "invalid"):
|
|
222
|
+
# A held golden is settled by a person through the golden side door
|
|
223
|
+
# and never re-derived (`promotion_blocker` says the same). Running the
|
|
224
|
+
# value check on it printed HTTP 404 on every audit for a key whose
|
|
225
|
+
# canonicalQuery read the model under test, `verify()` counted that
|
|
226
|
+
# error as drift, and `run_baseline` refused to start the arm -- with
|
|
227
|
+
# `--refresh` as the suggested remedy, which cannot fix an error.
|
|
228
|
+
return ("skipped",
|
|
229
|
+
f"{g['status']}: settled by a person through the golden side "
|
|
230
|
+
f"door, not re-derived", None)
|
|
221
231
|
if not q:
|
|
222
232
|
# A PROVISIONAL golden with no query is young, not wrong, and it is
|
|
223
233
|
# exactly what `eval-import` produces from a set that arrived as
|
|
@@ -243,6 +253,21 @@ def check_value(case: dict[str, Any], a: argparse.Namespace
|
|
|
243
253
|
rows, err = try_query(a.publisher, a.environment, a.truth_package,
|
|
244
254
|
a.truth_model, q)
|
|
245
255
|
if err:
|
|
256
|
+
if "not queryable" in err:
|
|
257
|
+
# The single most likely state of a set authored before its truth
|
|
258
|
+
# package existed: every key replays a packaged view (`run:
|
|
259
|
+
# orders -> ...`), and the truth server answers 404 "Query
|
|
260
|
+
# target is not queryable" 23 times with no hint that the query's
|
|
261
|
+
# root source is not a raw table. The message sent a reader to the
|
|
262
|
+
# server; the defect is in the golden.
|
|
263
|
+
m = re.match(r"\s*run:\s*([A-Za-z_]\w*)", q)
|
|
264
|
+
root = f"`{m.group(1)}`" if m else "its root source"
|
|
265
|
+
return ("error",
|
|
266
|
+
f"canonicalQuery reads {root}, which the truth package does "
|
|
267
|
+
f"not serve as queryable: either the truth model does not "
|
|
268
|
+
f"declare it (a key derived through the model under test, "
|
|
269
|
+
f"not the raw tables) or the query did not compile. "
|
|
270
|
+
f"Re-derive the key through the truth package", None)
|
|
246
271
|
return "error", err[:160], None
|
|
247
272
|
|
|
248
273
|
want = g.get("value")
|
|
@@ -518,6 +543,49 @@ def promotion_blocker(case: dict[str, Any], set_dir: pathlib.Path) -> str | None
|
|
|
518
543
|
return None
|
|
519
544
|
|
|
520
545
|
|
|
546
|
+
# A figure is a number a reader would check: 8,817.56, 943, 25%. Years and
|
|
547
|
+
# small ordinals are not -- "top 3", "2025" -- so they do not raise a finding.
|
|
548
|
+
FIGURE = re.compile(r"(?<![\w.])(\d{1,3}(?:,\d{3})+(?:\.\d+)?|\d+\.\d+|\d{3,})")
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def criteria_figure_findings(case: dict[str, Any]) -> list[str]:
|
|
552
|
+
"""Numbers quoted inside a `criteria` golden, which nothing can verify.
|
|
553
|
+
|
|
554
|
+
A criteria golden is exempt from both numeric checks and legitimately so:
|
|
555
|
+
it holds no value to re-derive, and the rubric-figure check compares
|
|
556
|
+
figures against the golden's ROWS, which it does not have. The clauses are
|
|
557
|
+
prose and prose cannot drift.
|
|
558
|
+
|
|
559
|
+
Its prose can still assert numbers, and then the exemption is a hole rather
|
|
560
|
+
than a simplification. This set shipped one: a criteria golden's note said
|
|
561
|
+
"by order count it is Amelia Cohen (80 orders)". Both halves were wrong --
|
|
562
|
+
at the grain the same rubric requires, the top by order count is the same
|
|
563
|
+
customer who tops spend, and 80 is three customers sharing a name, summed
|
|
564
|
+
by exactly the grouping the neighbouring case exists to forbid. Every check
|
|
565
|
+
passed it, because every check skips this kind of golden.
|
|
566
|
+
|
|
567
|
+
Reported as review, not failure: quoting a figure is often right, and the
|
|
568
|
+
point is that a human has to be the one who checked it.
|
|
569
|
+
"""
|
|
570
|
+
g = case.get("golden") or {}
|
|
571
|
+
if g.get("kind") != "criteria":
|
|
572
|
+
return []
|
|
573
|
+
out = []
|
|
574
|
+
for field, text in (("rubric", g.get("rubric")),
|
|
575
|
+
("verification.note", (g.get("verification") or {}).get("note"))):
|
|
576
|
+
if not isinstance(text, str):
|
|
577
|
+
continue
|
|
578
|
+
figs = sorted(set(FIGURE.findall(text)))
|
|
579
|
+
if figs:
|
|
580
|
+
out.append(f"review {case['qid']}: the criteria golden's {field} quotes "
|
|
581
|
+
f"{', '.join(figs[:6])}"
|
|
582
|
+
f"{' and more' if len(figs) > 6 else ''}, and NOTHING in this "
|
|
583
|
+
f"toolchain can check a figure in a criteria golden -- it has no "
|
|
584
|
+
f"rows for the rubric-figure check and no value to re-derive. "
|
|
585
|
+
f"Query each one at the grain this rubric requires, or drop it")
|
|
586
|
+
return out
|
|
587
|
+
|
|
588
|
+
|
|
521
589
|
def axis_findings(case: dict[str, Any], set_dir: pathlib.Path) -> list[str]:
|
|
522
590
|
g = case.get("golden") or {}
|
|
523
591
|
if g.get("kind") == "unanswerable":
|
|
@@ -1021,6 +1089,7 @@ def verify(set_dir: pathlib.Path, publisher: str, environment: str,
|
|
|
1021
1089
|
for c in chosen:
|
|
1022
1090
|
findings += rubric_number_findings(c)
|
|
1023
1091
|
findings += axis_findings(c, set_dir)
|
|
1092
|
+
findings += criteria_figure_findings(c)
|
|
1024
1093
|
findings += rubric_alternative_findings(chosen)
|
|
1025
1094
|
findings += stale_rubric_claims(chosen, model_definitions(model))
|
|
1026
1095
|
findings += unknown_name_findings(chosen, model_text(model))
|
|
@@ -1021,3 +1021,67 @@ class NumericRendering(unittest.TestCase):
|
|
|
1021
1021
|
|
|
1022
1022
|
if __name__ == "__main__":
|
|
1023
1023
|
unittest.main()
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
class HeldGoldensAreNotReDerived(unittest.TestCase):
|
|
1027
|
+
"""An `ambiguous` or `invalid` golden is a person's judgement. Running the
|
|
1028
|
+
value check on one reported an error on every audit, `verify()` counted the
|
|
1029
|
+
error as drift, and the arm refused to start."""
|
|
1030
|
+
|
|
1031
|
+
def setUp(self):
|
|
1032
|
+
self.a = argparse.Namespace(rewrite=False, publisher="http://x",
|
|
1033
|
+
environment="e", truth_package="t",
|
|
1034
|
+
truth_model="truth.malloy")
|
|
1035
|
+
|
|
1036
|
+
def case(self, status):
|
|
1037
|
+
return {"qid": "s-1", "golden": {"kind": "scalar", "status": status,
|
|
1038
|
+
"canonicalQuery": "run: model -> { aggregate: n }",
|
|
1039
|
+
"value": {"n": 1}}}
|
|
1040
|
+
|
|
1041
|
+
def test_a_held_golden_is_skipped_with_the_reason(self):
|
|
1042
|
+
for status in ("ambiguous", "invalid"):
|
|
1043
|
+
with self.subTest(status=status):
|
|
1044
|
+
with unittest.mock.patch.object(verify_goldens, "try_query") as tq:
|
|
1045
|
+
got, detail, rows = check_value(self.case(status), self.a)
|
|
1046
|
+
tq.assert_not_called()
|
|
1047
|
+
self.assertEqual(got, "skipped")
|
|
1048
|
+
self.assertIn(status, detail)
|
|
1049
|
+
self.assertIn("side door", detail)
|
|
1050
|
+
|
|
1051
|
+
def test_a_provisional_golden_with_a_query_still_runs(self):
|
|
1052
|
+
with unittest.mock.patch.object(verify_goldens, "try_query",
|
|
1053
|
+
return_value=([{"n": 1}], None)) as tq:
|
|
1054
|
+
got, _, _ = check_value(self.case("provisional"), self.a)
|
|
1055
|
+
tq.assert_called_once()
|
|
1056
|
+
self.assertEqual(got, "ok")
|
|
1057
|
+
|
|
1058
|
+
|
|
1059
|
+
class ANotQueryableKeyNamesItsRootSource(unittest.TestCase):
|
|
1060
|
+
"""A set authored before its truth package existed replays packaged views;
|
|
1061
|
+
the truth server answers 404 for each, and the bare message sent a reader
|
|
1062
|
+
to the server instead of the golden."""
|
|
1063
|
+
|
|
1064
|
+
def test_the_root_source_and_the_remedy_are_in_the_detail(self):
|
|
1065
|
+
a = argparse.Namespace(rewrite=False, publisher="http://x", environment="e",
|
|
1066
|
+
truth_package="t", truth_model="truth.malloy")
|
|
1067
|
+
case = {"qid": "s-2", "golden": {"kind": "scalar", "status": "provisional",
|
|
1068
|
+
"canonicalQuery": "run: orders -> { aggregate: n }",
|
|
1069
|
+
"value": {"n": 1}}}
|
|
1070
|
+
err = 'HTTP 404: {"code":404,"message":"Query target is not queryable."}'
|
|
1071
|
+
with unittest.mock.patch.object(verify_goldens, "try_query",
|
|
1072
|
+
return_value=(None, err)):
|
|
1073
|
+
got, detail, _ = check_value(case, a)
|
|
1074
|
+
self.assertEqual(got, "error")
|
|
1075
|
+
self.assertIn("`orders`", detail)
|
|
1076
|
+
self.assertIn("truth package", detail)
|
|
1077
|
+
|
|
1078
|
+
def test_any_other_error_is_reported_as_it_came(self):
|
|
1079
|
+
a = argparse.Namespace(rewrite=False, publisher="http://x", environment="e",
|
|
1080
|
+
truth_package="t", truth_model="truth.malloy")
|
|
1081
|
+
case = {"qid": "s-3", "golden": {"kind": "scalar", "status": "provisional",
|
|
1082
|
+
"canonicalQuery": "run: t_a -> { aggregate: n }",
|
|
1083
|
+
"value": {"n": 1}}}
|
|
1084
|
+
with unittest.mock.patch.object(verify_goldens, "try_query",
|
|
1085
|
+
return_value=(None, "HTTP 500: boom")):
|
|
1086
|
+
got, detail, _ = check_value(case, a)
|
|
1087
|
+
self.assertEqual((got, detail), ("error", "HTTP 500: boom"))
|
|
@@ -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.24
|
|
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.24
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Import questions into an eval set
|
|
@@ -13,7 +13,7 @@ On the case:
|
|
|
13
13
|
| field | from the source | when it is missing |
|
|
14
14
|
|---|---|---|
|
|
15
15
|
| `qid` | their id, if they gave one, prefixed with the set name | derive from the question: a slug of its first words plus a counter. Stable, because scores are keyed on it |
|
|
16
|
-
| `question` | the question text, byte for byte | it is not a case. Do not reconstruct a question from a criterion |
|
|
16
|
+
| `question` | the question text, byte for byte. One exception: a source hard-wrapped at a column width cannot be one line byte for byte, so a line break inside a question becomes one space, the seal is on the unwrapped text, and `sourceNote` says so | it is not a case. Do not reconstruct a question from a criterion |
|
|
17
17
|
| `questionSha` | never from the source | stamped at conversion by `import_cases.py --stamp` |
|
|
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 |
|
|
@@ -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.24
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Improve the Model
|
|
@@ -1,12 +1,12 @@
|
|
|
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.24
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# The judge
|
|
8
8
|
|
|
9
|
-
JUDGE_VERSION:
|
|
9
|
+
JUDGE_VERSION: 6
|
|
10
10
|
|
|
11
11
|
This skill IS the judge. One fresh judge subagent is spawned per attempt, with
|
|
12
12
|
this skill installed in its workspace and the case materials in its prompt. It
|
|
@@ -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
|
|
@@ -165,6 +186,29 @@ Output, exactly this shape:
|
|
|
165
186
|
total; the question had never asked for one. A rubric that means "campaign
|
|
166
187
|
total only" must say so as `REQUIRED`, and the question should say so too.
|
|
167
188
|
|
|
189
|
+
### Rubric markers
|
|
190
|
+
|
|
191
|
+
A case rubric marks its alternate readings and disclosures with the words
|
|
192
|
+
below, and each word fixes the verdict. Apply them as written; do not re-weigh
|
|
193
|
+
a reading the rubric has already classified. (`reference/writing-rubrics.md`
|
|
194
|
+
is where authors are told to use them; this table is the judge's half.)
|
|
195
|
+
|
|
196
|
+
| Marker | Verdict | Meaning |
|
|
197
|
+
|---|---|---|
|
|
198
|
+
| `PREFERRED` | `match` | The reading the golden encodes. |
|
|
199
|
+
| `ACCEPT` | `match` | Equally right: a different but faithful route to the same claim. Check the figure against the golden the way the clause says to. |
|
|
200
|
+
| `DIVERGENT` | `near_match` | Defensible and not what was asked for. Never `no_match`, however clearly the answer committed to it. |
|
|
201
|
+
| `WRONG` | `no_match` | Plausible and incorrect; the clause usually names the trap. |
|
|
202
|
+
| `REQUIRED` | omitted: `no_match` | A disclosure without which the number misleads. |
|
|
203
|
+
| `CREDITED` | omitted: `match` | Context a good analyst adds; its absence costs nothing. |
|
|
204
|
+
|
|
205
|
+
Measured on an unchanged answer, rubric and golden: an answer whose
|
|
206
|
+
recommended figure the rubric marked `DIVERGENT` scored `near_match` under
|
|
207
|
+
one judge and `no_match` under the next, because the judge had never been
|
|
208
|
+
told what the word meant and weighed the commitment instead. An unmarked
|
|
209
|
+
clause is `CREDITED` (the author's bug, not yours to repair by inventing a
|
|
210
|
+
requirement).
|
|
211
|
+
|
|
168
212
|
### Anchors
|
|
169
213
|
|
|
170
214
|
- **match**: question "total sales by category"; golden 8 rows
|
|
@@ -40,6 +40,14 @@ Rules that follow from this:
|
|
|
40
40
|
must say "for the campaign as a whole". If it does not, the rubric must accept
|
|
41
41
|
a correct figure at any stated grain (judge rule 11). A rubric that quietly
|
|
42
42
|
assumes the golden's grain fails correct answers.
|
|
43
|
+
- **Fix the cardinality in the question, or accept any prefix in the rubric.**
|
|
44
|
+
"The most", "the top ones", "most often" name no N; the truth query needs
|
|
45
|
+
one, so the golden fixes 10 or 20 or 25 and an answer that shows five
|
|
46
|
+
correct rows reads as missing fifteen. Measured over one 24-case set, that
|
|
47
|
+
class alone produced a `no_match`, a flip and two `near_match` verdicts on
|
|
48
|
+
answers whose rows were a correct prefix of the golden's. Either write the N
|
|
49
|
+
into the question, or mark the rubric `ACCEPT the first N of the golden's
|
|
50
|
+
order for any N of at least five`, and say which order the golden is in.
|
|
43
51
|
- **A right value plus a missing `CREDITED` disclosure is a `match`.** Not a
|
|
44
52
|
near match. Do not deduct for it.
|
|
45
53
|
- **`DIVERGENT` is about definitions, not arithmetic.** A clause permitting a
|