@malloy-publisher/skills 0.1.19 → 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 +6 -7
- package/skills/malloy-analysis/SKILL.md +22 -45
- package/skills/malloy-analysis-pitfalls/SKILL.md +21 -6
- package/skills/malloy-analysis-report/SKILL.md +1 -1
- package/skills/malloy-analyze/SKILL.md +1 -1
- package/skills/malloy-charts/SKILL.md +2 -2
- 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 +2 -2
- package/skills/malloy-gotchas-queries/SKILL.md +2 -2
- package/skills/malloy-gotchas-rendering/SKILL.md +2 -2
- 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-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 +2 -2
- package/skills/malloy-phrase-detection/SKILL.md +12 -7
- package/skills/malloy-publish/SKILL.md +1 -1
- package/skills/malloy-queries/SKILL.md +2 -2
- package/skills/malloy-review/SKILL.md +1 -1
- package/skills/malloy-scope/SKILL.md +7 -4
|
@@ -59,13 +59,43 @@ def outcome(verdict: str | None) -> str:
|
|
|
59
59
|
return "neither"
|
|
60
60
|
|
|
61
61
|
|
|
62
|
+
# A golden a person has established is WRONG. Its verdict says nothing about
|
|
63
|
+
# the model -- the answer was scored against a key that does not hold -- so it
|
|
64
|
+
# leaves every aggregate.
|
|
65
|
+
EXCLUDED_GOLD = {"verified_wrong"}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def counts_toward_score(gold_status: str | None) -> bool:
|
|
69
|
+
"""Does this score belong in the run's aggregates?
|
|
70
|
+
|
|
71
|
+
Exported for the same reason `outcome` is. `run_baseline.py` dropped
|
|
72
|
+
`verified_wrong` before counting, `eval_run.malloy` did not, and the
|
|
73
|
+
package's own doc comment on `gold_status` said it did -- so one
|
|
74
|
+
`verified_wrong` golden made the printed pass rate and the notebook's
|
|
75
|
+
`pass_rate` disagree (92.31% against 91.67% on a real 13-case run) with
|
|
76
|
+
nothing to say which was right. One rule, three readers: this function,
|
|
77
|
+
the `counts` column `build_run_package.py` writes from it, and the
|
|
78
|
+
measures in `eval_run.malloy` that filter on that column.
|
|
79
|
+
"""
|
|
80
|
+
return gold_status not in EXCLUDED_GOLD
|
|
81
|
+
|
|
82
|
+
|
|
62
83
|
def verdicts(run: Path) -> dict[str, dict[str, Any]]:
|
|
63
84
|
"""qid -> the scored outcome, for cases this run actually scored."""
|
|
64
85
|
out: dict[str, dict[str, Any]] = {}
|
|
86
|
+
queries: dict[str, str | None] = {}
|
|
65
87
|
for line in (run / "events.jsonl").read_text().splitlines():
|
|
66
88
|
if not line.strip():
|
|
67
89
|
continue
|
|
68
90
|
e = json.loads(line)
|
|
91
|
+
if e.get("kind") == "attempt":
|
|
92
|
+
# The query the arm actually ran. A flip is a matched pair -- same
|
|
93
|
+
# question, same model, one right answer and one wrong one -- and
|
|
94
|
+
# the diff between the two queries is what isolates the cause. It
|
|
95
|
+
# was not read here, so the richest evidence in the run was the one
|
|
96
|
+
# thing the flip table did not print.
|
|
97
|
+
queries[e["qid"]] = e.get("final_query")
|
|
98
|
+
continue
|
|
69
99
|
if e.get("kind") != "score":
|
|
70
100
|
continue
|
|
71
101
|
v = e.get("verdict")
|
|
@@ -73,6 +103,7 @@ def verdicts(run: Path) -> dict[str, dict[str, Any]]:
|
|
|
73
103
|
"verdict": v,
|
|
74
104
|
"confidence": e.get("confidence"),
|
|
75
105
|
"reason": (e.get("reason") or "")[:200],
|
|
106
|
+
"final_query": queries.get(e["qid"]),
|
|
76
107
|
# near_match and needs_human are neither: counting either as a fail
|
|
77
108
|
# would manufacture a flip every time the judge hedged in one run
|
|
78
109
|
# and not the other.
|
|
@@ -82,6 +113,30 @@ def verdicts(run: Path) -> dict[str, dict[str, Any]]:
|
|
|
82
113
|
return out
|
|
83
114
|
|
|
84
115
|
|
|
116
|
+
def query_diff(a: dict[str, Any], b: dict[str, Any], la: str, lb: str) -> str:
|
|
117
|
+
"""The two queries a flipped case ran, side by side.
|
|
118
|
+
|
|
119
|
+
The count says a case moved; this says what moved it. On a real set five
|
|
120
|
+
flips over 28 cases read as an 18% churn rate, and the five turned out to
|
|
121
|
+
be one question asked two ways -- two discrete query idioms, one returning
|
|
122
|
+
443 rows and the other 2. Reading the diff would have handed over the
|
|
123
|
+
finding; averaging the five hid it. The queries were already in the ledger
|
|
124
|
+
and this table had never printed them.
|
|
125
|
+
|
|
126
|
+
Identical queries are worth saying too: then the flip is downstream of the
|
|
127
|
+
query, in the judge or in the data, and that is a different search.
|
|
128
|
+
"""
|
|
129
|
+
qa, qb = (a.get("final_query") or "").strip(), (b.get("final_query") or "").strip()
|
|
130
|
+
if not qa and not qb:
|
|
131
|
+
return " (neither arm recorded a final query)"
|
|
132
|
+
if qa == qb:
|
|
133
|
+
return (" both arms ran the SAME query, so the flip is downstream "
|
|
134
|
+
"of it:\n the judge, the rubric, or non-determinism in the "
|
|
135
|
+
"data.")
|
|
136
|
+
return (f" {la} ran:\n " + qa.replace("\n", "\n ") +
|
|
137
|
+
f"\n {lb} ran:\n " + qb.replace("\n", "\n "))
|
|
138
|
+
|
|
139
|
+
|
|
85
140
|
def cost(run: Path) -> dict[str, float]:
|
|
86
141
|
tot: dict[str, float] = {"usd": 0.0, "turns": 0.0, "seconds": 0.0}
|
|
87
142
|
for line in (run / "events.jsonl").read_text().splitlines():
|
|
@@ -114,6 +169,47 @@ COMPARABLE = ("datasetVersion", "datasetSha", "judgeVersion", "rubricSha",
|
|
|
114
169
|
"retrievalMode", "modelGitSha", "targetVersion")
|
|
115
170
|
|
|
116
171
|
|
|
172
|
+
def completeness_note(ca: dict, cb: dict, la: str, lb: str,
|
|
173
|
+
A: dict, B: dict) -> int:
|
|
174
|
+
"""Say which cases one arm excluded and the other scored. Never refuses.
|
|
175
|
+
|
|
176
|
+
This REPLACED a refusal, and the refusal was wrong. It rested on the claim
|
|
177
|
+
that an excluded case "reads as a flip", and it does not: a flip requires a
|
|
178
|
+
real pass or fail on BOTH sides (`a_only` / `b_only` below), so a case
|
|
179
|
+
carrying `verdict: null` on either side lands in `unscored` and contributes
|
|
180
|
+
to no flip count. The pairing already handles this, and the header already
|
|
181
|
+
prints "N cases, M scored in both, K not". Refusing a 99-versus-100 pair
|
|
182
|
+
threw away 99 good comparisons to avoid a distortion that was not there.
|
|
183
|
+
|
|
184
|
+
What IS worth saying is the asymmetry, which nothing named before. The
|
|
185
|
+
excluded cases are not a random sample: an attempt truncates BECAUSE it ran
|
|
186
|
+
long, so the cases one arm drops are its hard ones, and the surviving
|
|
187
|
+
comparison is over an easier subset than the case list suggests. That is a
|
|
188
|
+
caveat to carry into the number, not a reason to withhold it.
|
|
189
|
+
|
|
190
|
+
An `aborted` arm needs nothing here either: it stopped early, so its cases
|
|
191
|
+
are absent rather than unscored, and the existing "the runs do not cover
|
|
192
|
+
the same cases" check already exits 1 on it.
|
|
193
|
+
"""
|
|
194
|
+
for label, cfg, mine, theirs in ((la, ca, A, B), (lb, cb, B, A)):
|
|
195
|
+
excluded = [q for q in sorted(set(mine) & set(theirs))
|
|
196
|
+
if mine[q]["passed"] is None
|
|
197
|
+
and theirs[q]["passed"] is not None]
|
|
198
|
+
if not excluded:
|
|
199
|
+
continue
|
|
200
|
+
why = ", ".join(
|
|
201
|
+
[f"{len(cfg.get('truncated') or [])} truncated"]
|
|
202
|
+
* bool(cfg.get("truncated"))
|
|
203
|
+
+ [f"{len(cfg.get('contaminated') or [])} contaminated"]
|
|
204
|
+
* bool(cfg.get("contaminated"))) or "unscored"
|
|
205
|
+
print(f"\n ! {label} left {len(excluded)} case(s) unscored that "
|
|
206
|
+
f"the other arm scored ({why}): {', '.join(excluded)}")
|
|
207
|
+
print(" They are out of the flips above, so the comparison holds --"
|
|
208
|
+
" but an attempt truncates BECAUSE it ran long, so these are not"
|
|
209
|
+
" a random sample of the set.")
|
|
210
|
+
return 0
|
|
211
|
+
|
|
212
|
+
|
|
117
213
|
def retrieval_gate(ca: dict, cb: dict, la: str, lb: str,
|
|
118
214
|
allow: bool) -> int:
|
|
119
215
|
"""Refuse a pair whose runs used different retrievers.
|
|
@@ -402,13 +498,21 @@ def main() -> int:
|
|
|
402
498
|
f" ({', '.join(f'{v} {k}' for k, v in sorted(tally.items()))})")
|
|
403
499
|
|
|
404
500
|
if a_only or b_only:
|
|
405
|
-
print(f"\nthe
|
|
501
|
+
print(f"\nthe disagreement set\n--------------------")
|
|
502
|
+
print(" Read these before averaging them. A flip is a case where the "
|
|
503
|
+
"agent found\n two paths and the model did not make one of them "
|
|
504
|
+
"obviously right, which is\n a model-quality signal and a "
|
|
505
|
+
"matched pair: same question, same model, one\n right answer "
|
|
506
|
+
"and one wrong one. The band says how much movement there is;\n"
|
|
507
|
+
" these say what it IS.\n")
|
|
406
508
|
for q in a_only:
|
|
407
509
|
print(f" {q}\n {la}: {A[q]['verdict']} -> "
|
|
408
510
|
f"{lb}: {B[q]['verdict']}\n {B[q]['reason'][:150]}")
|
|
511
|
+
print(query_diff(A[q], B[q], la, lb))
|
|
409
512
|
for q in b_only:
|
|
410
513
|
print(f" {q}\n {la}: {A[q]['verdict']} -> "
|
|
411
|
-
f"{lb}: {B[q]['verdict']}\n {
|
|
514
|
+
f"{lb}: {B[q]['verdict']}\n {A[q]['reason'][:150]}")
|
|
515
|
+
print(query_diff(A[q], B[q], la, lb))
|
|
412
516
|
|
|
413
517
|
if a_args.targets:
|
|
414
518
|
targeted_report(a_args, A, B, la, lb,
|
|
@@ -443,6 +547,7 @@ def main() -> int:
|
|
|
443
547
|
|
|
444
548
|
gate = retrieval_gate(cfg_a, cfg_b, la, lb,
|
|
445
549
|
a_args.allow_retrieval_mismatch)
|
|
550
|
+
completeness_note(cfg_a, cfg_b, la, lb, A, B)
|
|
446
551
|
|
|
447
552
|
ca, cb = cost(a_args.a), cost(a_args.b)
|
|
448
553
|
print(f"\ncost\n----")
|
|
@@ -146,6 +146,70 @@ class Calibration(unittest.TestCase):
|
|
|
146
146
|
self.assertIn("sonnet / opus", block)
|
|
147
147
|
|
|
148
148
|
|
|
149
|
+
class CompletenessNote(unittest.TestCase):
|
|
150
|
+
"""The asymmetry between two arms' unscored cases, reported not refused.
|
|
151
|
+
|
|
152
|
+
This replaced a refusal that was based on a false claim: that a case one
|
|
153
|
+
arm excluded "reads as a flip". It cannot. A flip needs a real pass or fail
|
|
154
|
+
on BOTH sides, so a null verdict on either lands in `unscored` and counts
|
|
155
|
+
toward no flip. Refusing a 99-versus-100 pair discarded 99 good
|
|
156
|
+
comparisons to avoid a distortion that was not there.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
def verdicts(self, **qids):
|
|
160
|
+
return {q: {"verdict": v, "passed": p, "outcome": "x", "reason": "",
|
|
161
|
+
"confidence": None}
|
|
162
|
+
for q, (v, p) in qids.items()}
|
|
163
|
+
|
|
164
|
+
def test_an_excluded_case_cannot_become_a_flip(self):
|
|
165
|
+
# The claim the refusal rested on, tested directly against the pairing
|
|
166
|
+
# rule the report uses.
|
|
167
|
+
A = self.verdicts(q1=("match", True), q2=(None, None))
|
|
168
|
+
B = self.verdicts(q1=("match", True), q2=("no_match", False))
|
|
169
|
+
shared = sorted(A)
|
|
170
|
+
a_only = [q for q in shared if A[q]["passed"] and B[q]["passed"] is False]
|
|
171
|
+
b_only = [q for q in shared if A[q]["passed"] is False and B[q]["passed"]]
|
|
172
|
+
unscored = [q for q in shared
|
|
173
|
+
if A[q]["passed"] is None or B[q]["passed"] is None]
|
|
174
|
+
self.assertEqual(a_only + b_only, [])
|
|
175
|
+
self.assertEqual(unscored, ["q2"])
|
|
176
|
+
|
|
177
|
+
def test_it_never_refuses(self):
|
|
178
|
+
A = self.verdicts(q1=("match", True), q2=(None, None))
|
|
179
|
+
B = self.verdicts(q1=("match", True), q2=("no_match", False))
|
|
180
|
+
self.assertEqual(
|
|
181
|
+
ft.completeness_note({"status": "incomplete", "truncated": ["q2"]},
|
|
182
|
+
{"status": "complete"}, "a", "b", A, B), 0)
|
|
183
|
+
|
|
184
|
+
def test_two_complete_arms_say_nothing(self):
|
|
185
|
+
A = B = self.verdicts(q1=("match", True))
|
|
186
|
+
out = io.StringIO()
|
|
187
|
+
with contextlib.redirect_stdout(out):
|
|
188
|
+
ft.completeness_note({}, {}, "a", "b", A, B)
|
|
189
|
+
self.assertEqual(out.getvalue(), "")
|
|
190
|
+
|
|
191
|
+
def test_it_names_the_cases_and_why(self):
|
|
192
|
+
A = self.verdicts(q1=("match", True), q2=(None, None))
|
|
193
|
+
B = self.verdicts(q1=("match", True), q2=("no_match", False))
|
|
194
|
+
out = io.StringIO()
|
|
195
|
+
with contextlib.redirect_stdout(out):
|
|
196
|
+
ft.completeness_note({"truncated": ["q2"]}, {}, "arm-a", "arm-b",
|
|
197
|
+
A, B)
|
|
198
|
+
body = out.getvalue()
|
|
199
|
+
self.assertIn("arm-a left 1 case(s) unscored", body)
|
|
200
|
+
self.assertIn("1 truncated", body)
|
|
201
|
+
self.assertIn("q2", body)
|
|
202
|
+
# The caveat that is actually true: the dropped cases are the long ones.
|
|
203
|
+
self.assertIn("BECAUSE it ran long", body)
|
|
204
|
+
|
|
205
|
+
def test_the_answerer_model_is_already_a_pin(self):
|
|
206
|
+
# Not this note's job, and worth pinning so nobody adds a second
|
|
207
|
+
# mechanism: an arm on one model against another is already reported
|
|
208
|
+
# as a differing pin. That was the largest uncontrolled variable in the
|
|
209
|
+
# run this came from.
|
|
210
|
+
self.assertIn("answererModel", ft.COMPARABLE)
|
|
211
|
+
|
|
212
|
+
|
|
149
213
|
class Outcome(unittest.TestCase):
|
|
150
214
|
def test_near_match_is_neither(self):
|
|
151
215
|
self.assertEqual(ft.outcome("near_match"), "neither")
|
|
@@ -153,6 +217,107 @@ class Outcome(unittest.TestCase):
|
|
|
153
217
|
def test_an_unknown_verdict_is_never_a_fail(self):
|
|
154
218
|
self.assertEqual(ft.outcome("invented"), "neither")
|
|
155
219
|
|
|
220
|
+
class CountsTowardScore(unittest.TestCase):
|
|
221
|
+
"""One rule, three readers, because two of them had drifted.
|
|
222
|
+
|
|
223
|
+
`run_baseline.py` dropped a `verified_wrong` golden before counting and
|
|
224
|
+
`eval_run.malloy` did not, while the package's own doc comment on
|
|
225
|
+
`gold_status` said it did. On a real 13-case run with one such golden the
|
|
226
|
+
printed rate read 91.67% and the notebook's `pass_rate` read 92.31%, off
|
|
227
|
+
the same rows, with nothing to say which was right.
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
def test_a_verified_wrong_golden_leaves_the_aggregates(self):
|
|
231
|
+
self.assertFalse(ft.counts_toward_score("verified_wrong"))
|
|
232
|
+
|
|
233
|
+
def test_every_other_status_counts(self):
|
|
234
|
+
for s in ("verified", "verified_benign", "suspect", "provisional",
|
|
235
|
+
"ambiguous", "invalid"):
|
|
236
|
+
with self.subTest(status=s):
|
|
237
|
+
self.assertTrue(ft.counts_toward_score(s))
|
|
238
|
+
|
|
239
|
+
def test_an_absent_status_counts(self):
|
|
240
|
+
# A run predating gold_status must not silently lose every case.
|
|
241
|
+
self.assertTrue(ft.counts_toward_score(None))
|
|
242
|
+
|
|
243
|
+
def test_run_baseline_reads_this_rule_rather_than_restating_it(self):
|
|
244
|
+
src = (pathlib.Path(__file__).resolve().parent
|
|
245
|
+
/ "run_baseline.py").read_text()
|
|
246
|
+
# The AGGREGATE site reads the helper. The other `verified_wrong`
|
|
247
|
+
# test in that file is a different question -- whether a case is
|
|
248
|
+
# scorable at all, from the golden's own status, before an attempt --
|
|
249
|
+
# and is correctly separate.
|
|
250
|
+
self.assertIn('counts_toward_score(v.get("gold_status"))', src)
|
|
251
|
+
self.assertNotIn('v.get("gold_status") == "verified_wrong"', src)
|
|
252
|
+
|
|
253
|
+
def test_the_flattener_writes_it_as_a_column(self):
|
|
254
|
+
src = (pathlib.Path(__file__).resolve().parent
|
|
255
|
+
/ "build_run_package.py").read_text()
|
|
256
|
+
self.assertIn('"counts": counts_toward_score(', src)
|
|
257
|
+
self.assertIn('"counts", "reason"', src)
|
|
258
|
+
|
|
259
|
+
def test_the_malloy_measures_filter_on_that_column(self):
|
|
260
|
+
malloy = (pathlib.Path(__file__).resolve().parent.parent
|
|
261
|
+
/ "templates" / "eval-run-package" / "eval_run.malloy").read_text()
|
|
262
|
+
for m in ("confident_count", "passed", "failed", "near_matches",
|
|
263
|
+
"needs_human"):
|
|
264
|
+
line = next(l for l in malloy.splitlines()
|
|
265
|
+
if l.strip().startswith(f"{m} is count()"))
|
|
266
|
+
self.assertIn("counts = 'true'", line, f"{m} does not exclude it")
|
|
267
|
+
# Written as a string because flatten() serialises a bool that way.
|
|
268
|
+
self.assertIn("public: counts", malloy)
|
|
269
|
+
|
|
270
|
+
class DisagreementSet(unittest.TestCase):
|
|
271
|
+
"""A flip is a matched pair, and the table has to print the pair.
|
|
272
|
+
|
|
273
|
+
The count says a case moved; the two queries say what moved it. On a real
|
|
274
|
+
set five flips over 28 cases read as an 18% churn rate and were one
|
|
275
|
+
question asked two ways, the wrong idiom returning 2 rows against 443.
|
|
276
|
+
That took a day of transcript reading because this table printed only the
|
|
277
|
+
verdicts.
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
def test_it_prints_both_queries(self):
|
|
281
|
+
out = ft.query_diff({"final_query": "run: a -> { x }"},
|
|
282
|
+
{"final_query": "run: b -> { y }"}, "arm1", "arm2")
|
|
283
|
+
self.assertIn("run: a -> { x }", out)
|
|
284
|
+
self.assertIn("run: b -> { y }", out)
|
|
285
|
+
self.assertIn("arm1", out)
|
|
286
|
+
self.assertIn("arm2", out)
|
|
287
|
+
|
|
288
|
+
def test_an_identical_query_is_called_out_as_a_different_finding(self):
|
|
289
|
+
q = "run: same -> { x }"
|
|
290
|
+
out = ft.query_diff({"final_query": q}, {"final_query": q}, "a", "b")
|
|
291
|
+
self.assertIn("SAME query", out)
|
|
292
|
+
self.assertIn("downstream", out)
|
|
293
|
+
|
|
294
|
+
def test_a_missing_query_says_so_rather_than_printing_nothing(self):
|
|
295
|
+
out = ft.query_diff({}, {}, "a", "b")
|
|
296
|
+
self.assertIn("neither arm recorded", out)
|
|
297
|
+
|
|
298
|
+
def test_one_side_missing_still_shows_the_other(self):
|
|
299
|
+
out = ft.query_diff({"final_query": "run: a"}, {}, "a", "b")
|
|
300
|
+
self.assertIn("run: a", out)
|
|
301
|
+
|
|
302
|
+
def test_verdicts_carries_the_final_query(self):
|
|
303
|
+
run = pathlib.Path(tempfile.mkdtemp()) / "r"
|
|
304
|
+
run.mkdir()
|
|
305
|
+
(run / "events.jsonl").write_text(
|
|
306
|
+
json.dumps({"kind": "attempt", "qid": "q1",
|
|
307
|
+
"final_query": "run: flights -> { x }"}) + "\n" +
|
|
308
|
+
json.dumps({"kind": "score", "qid": "q1", "verdict": "match"}) + "\n")
|
|
309
|
+
v = ft.verdicts(run)
|
|
310
|
+
self.assertEqual(v["q1"]["final_query"], "run: flights -> { x }")
|
|
311
|
+
|
|
312
|
+
def test_a_run_with_no_attempt_event_still_scores(self):
|
|
313
|
+
# Older runs, and a --rebuild that wrote scores only.
|
|
314
|
+
run = pathlib.Path(tempfile.mkdtemp()) / "r"
|
|
315
|
+
run.mkdir()
|
|
316
|
+
(run / "events.jsonl").write_text(
|
|
317
|
+
json.dumps({"kind": "score", "qid": "q1", "verdict": "match"}) + "\n")
|
|
318
|
+
v = ft.verdicts(run)
|
|
319
|
+
self.assertTrue(v["q1"]["passed"])
|
|
320
|
+
self.assertIsNone(v["q1"]["final_query"])
|
|
156
321
|
|
|
157
322
|
if __name__ == "__main__":
|
|
158
323
|
unittest.main()
|