@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.
Files changed (79) hide show
  1. package/package.json +1 -1
  2. package/skills/eval-answer/SKILL.md +45 -5
  3. package/skills/eval-answer/reference/definition-ledger.md +305 -0
  4. package/skills/eval-answer/reference/ledger-schema.md +36 -7
  5. package/skills/eval-answer/scripts/check_coverage.py +12 -21
  6. package/skills/eval-answer/scripts/check_coverage_test.py +22 -0
  7. package/skills/eval-answer/scripts/check_findable.py +386 -0
  8. package/skills/eval-answer/scripts/check_findable_test.py +326 -0
  9. package/skills/eval-answer/scripts/json_scan.py +43 -0
  10. package/skills/eval-answer/scripts/ledger.py +63 -7
  11. package/skills/eval-answer/scripts/ledger_test.py +7 -0
  12. package/skills/eval-answer/scripts/mcp_payload.py +111 -2
  13. package/skills/eval-answer/scripts/mcp_payload_test.py +73 -2
  14. package/skills/eval-answer/scripts/publisher_rest.py +10 -4
  15. package/skills/eval-answer/scripts/publisher_rest_test.py +56 -0
  16. package/skills/eval-answer/scripts/score_retrieval.py +326 -41
  17. package/skills/eval-answer/scripts/score_retrieval_test.py +537 -15
  18. package/skills/eval-answer/scripts/verify_definitions.py +752 -0
  19. package/skills/eval-answer/scripts/verify_definitions_test.py +820 -0
  20. package/skills/eval-answer/scripts/verify_goldens.py +336 -22
  21. package/skills/eval-answer/scripts/verify_goldens_test.py +261 -2
  22. package/skills/eval-diagnose/SKILL.md +47 -2
  23. package/skills/eval-diagnose/scripts/cluster_failures.py +57 -14
  24. package/skills/eval-diagnose/scripts/cluster_failures_test.py +127 -0
  25. package/skills/eval-diagnose/scripts/diagnose.py +471 -42
  26. package/skills/eval-diagnose/scripts/diagnose_test.py +421 -0
  27. package/skills/eval-import/SKILL.md +180 -1
  28. package/skills/eval-import/reference/case-format.md +1 -1
  29. package/skills/eval-improve/SKILL.md +1 -1
  30. package/skills/eval-improve/scripts/improve.py +29 -3
  31. package/skills/eval-improve/scripts/improve_test.py +51 -0
  32. package/skills/eval-judge/SKILL.md +1 -1
  33. package/skills/eval-loop/SKILL.md +112 -3
  34. package/skills/eval-loop/reference/measurement.md +44 -4
  35. package/skills/eval-loop/reference/running-a-run.md +5 -1
  36. package/skills/eval-loop/scripts/agent_harness.py +21 -20
  37. package/skills/eval-loop/scripts/agent_harness_test.py +136 -0
  38. package/skills/eval-loop/scripts/build_run_package.py +9 -3
  39. package/skills/eval-loop/scripts/flip_table.py +107 -2
  40. package/skills/eval-loop/scripts/flip_table_test.py +165 -0
  41. package/skills/eval-loop/scripts/run_baseline.py +956 -97
  42. package/skills/eval-loop/scripts/run_baseline_test.py +799 -1
  43. package/skills/eval-loop/templates/eval-run-package/README.md +19 -5
  44. package/skills/eval-loop/templates/eval-run-package/eval_run.malloy +48 -19
  45. package/skills/eval-loop/templates/eval-run-package/eval_run.malloynb +11 -4
  46. package/skills/eval-loop/templates/eval-run-package/public/app.js +12 -4
  47. package/skills/eval-report/SKILL.md +362 -0
  48. package/skills/malloy/SKILL.md +6 -7
  49. package/skills/malloy-analysis/SKILL.md +22 -45
  50. package/skills/malloy-analysis-pitfalls/SKILL.md +21 -6
  51. package/skills/malloy-analysis-report/SKILL.md +1 -1
  52. package/skills/malloy-analyze/SKILL.md +1 -1
  53. package/skills/malloy-charts/SKILL.md +2 -2
  54. package/skills/malloy-dashboards/SKILL.md +1 -1
  55. package/skills/malloy-debug/SKILL.md +1 -1
  56. package/skills/malloy-define/SKILL.md +1 -1
  57. package/skills/malloy-discover/SKILL.md +1 -1
  58. package/skills/malloy-document/SKILL.md +1 -1
  59. package/skills/malloy-getting-started/SKILL.md +1 -1
  60. package/skills/malloy-gotchas-modeling/SKILL.md +2 -2
  61. package/skills/malloy-gotchas-queries/SKILL.md +2 -2
  62. package/skills/malloy-gotchas-rendering/SKILL.md +2 -2
  63. package/skills/malloy-html-data-app-embedding/SKILL.md +1 -1
  64. package/skills/malloy-html-data-app-runtime/SKILL.md +2 -2
  65. package/skills/malloy-html-data-apps/SKILL.md +1 -1
  66. package/skills/malloy-lookml-review/SKILL.md +1 -1
  67. package/skills/malloy-materialization/SKILL.md +1 -1
  68. package/skills/malloy-materialization-tuning/SKILL.md +1 -1
  69. package/skills/malloy-model/SKILL.md +1 -1
  70. package/skills/malloy-model-as-you-go/SKILL.md +1 -1
  71. package/skills/malloy-modeling/SKILL.md +1 -1
  72. package/skills/malloy-notebook-chat/SKILL.md +1 -1
  73. package/skills/malloy-notebooks/SKILL.md +1 -1
  74. package/skills/malloy-patterns/SKILL.md +2 -2
  75. package/skills/malloy-phrase-detection/SKILL.md +12 -7
  76. package/skills/malloy-publish/SKILL.md +1 -1
  77. package/skills/malloy-queries/SKILL.md +2 -2
  78. package/skills/malloy-review/SKILL.md +1 -1
  79. package/skills/malloy-scope/SKILL.md +7 -4
@@ -0,0 +1,820 @@
1
+ #!/usr/bin/env python3
2
+ """Tests for verify_definitions.py. Stdlib only: python3 verify_definitions_test.py
3
+
4
+ The thing these guard is not "does it compute a number" but "does it ever claim
5
+ to have validated something it did not". Every `unchecked` path below is a case
6
+ where saying `agrees` would be the tool committing the error it exists to find.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import json
12
+ import os
13
+ import pathlib
14
+ import shutil
15
+ import sys
16
+ import tempfile
17
+ import unittest
18
+ from unittest import mock
19
+
20
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
21
+
22
+ import verify_definitions as vd # noqa: E402
23
+
24
+ MODEL = """\
25
+ source: order_items is duckdb.table('data/order_items.parquet') extend {
26
+ join_one: inventory_items is inventory_items on inventory_item_id = inventory_items.id
27
+ measure:
28
+ total_sales is sale_price.sum()
29
+ order_item_count is count()
30
+ total_sales_2022 is total_sales { where: year(created_at) = 2022 }
31
+ total_cost_through_join is inventory_items.cost.sum()
32
+ worst_margin is inventory_items.cost.min()
33
+ dimension:
34
+ order_year is year(created_at)
35
+ }
36
+ """
37
+
38
+
39
+ def write(text: str) -> pathlib.Path:
40
+ d = pathlib.Path(tempfile.mkdtemp())
41
+ p = d / "m.malloy"
42
+ p.write_text(text)
43
+ return p
44
+
45
+
46
+ def by_name(recs):
47
+ return {r["name"]: r for r in recs}
48
+
49
+
50
+ class TheLedgerItBuilds(unittest.TestCase):
51
+ def setUp(self):
52
+ self.model = write(MODEL)
53
+ self.recs = by_name(vd.records(self.model, recursive=False))
54
+
55
+ def tearDown(self):
56
+ shutil.rmtree(self.model.parent, ignore_errors=True)
57
+
58
+ def test_a_measure_is_source_qualified(self):
59
+ # The flat `dict[name]` this replaced resolved a same-named field in
60
+ # two sources to whichever file was read first.
61
+ self.assertEqual(self.recs["total_sales"]["entityId"],
62
+ "measure:order_items:total_sales")
63
+
64
+ def test_a_definition_records_what_it_builds_on(self):
65
+ self.assertIn("measure:order_items:total_sales",
66
+ self.recs["total_sales_2022"]["depends"])
67
+
68
+ def test_a_leaf_definition_depends_on_nothing_defined(self):
69
+ self.assertEqual(self.recs["total_sales"]["depends"], [])
70
+
71
+ def test_views_and_joins_are_not_checkable_definitions(self):
72
+ # A view composes measures that are checked on their own; a join is
73
+ # structure rather than a value.
74
+ self.assertNotIn("inventory_items", self.recs)
75
+
76
+
77
+ TWO_SOURCES = """\
78
+ source: source_a is duckdb.table('data/a.parquet') extend {
79
+ measure:
80
+ total_sales is sale_price.sum()
81
+ }
82
+ source: source_b is duckdb.table('data/b.parquet') extend {
83
+ measure:
84
+ total_sales is price.sum()
85
+ doubled is total_sales * 2
86
+ borrowed is source_a.total_sales * 3
87
+ }
88
+ """
89
+
90
+
91
+ class DependenciesResolveInTheSourceThatOwnsThem(unittest.TestCase):
92
+ """A bare word names a field of the DECLARING source.
93
+
94
+ Resolving it against a flat `dict[name]` linked `source_b.doubled` to
95
+ `source_a.total_sales` on a model where the two sources both declare the
96
+ name -- and, worse, on one where only the OTHER source declares it, giving
97
+ a dependant a dependency its own source does not have. Either way the sha
98
+ chain hashes the wrong expression, so `stale_ids` marks the wrong rows.
99
+ """
100
+
101
+ def setUp(self):
102
+ self.model = write(TWO_SOURCES)
103
+ self.recs = {r["entityId"]: r
104
+ for r in vd.records(self.model, recursive=False)}
105
+
106
+ def tearDown(self):
107
+ shutil.rmtree(self.model.parent, ignore_errors=True)
108
+
109
+ def test_a_bare_word_resolves_to_its_own_sources_field(self):
110
+ self.assertEqual(self.recs["measure:source_b:doubled"]["depends"],
111
+ ["measure:source_b:total_sales"])
112
+
113
+ def test_a_dotted_path_resolves_in_the_source_it_names(self):
114
+ self.assertEqual(self.recs["measure:source_b:borrowed"]["depends"],
115
+ ["measure:source_a:total_sales"])
116
+
117
+ def test_a_name_only_the_other_source_declares_is_not_a_dependency(self):
118
+ p = write(
119
+ "source: source_a is duckdb.table('data/a.parquet') extend {\n"
120
+ " measure:\n total_sales is sale_price.sum()\n}\n"
121
+ "source: source_b is duckdb.table('data/b.parquet') extend {\n"
122
+ " dimension:\n doubled is total_sales * 2\n}\n")
123
+ try:
124
+ recs = {r["entityId"]: r for r in vd.records(p, recursive=False)}
125
+ self.assertEqual(recs["dimension:source_b:doubled"]["depends"], [])
126
+ finally:
127
+ shutil.rmtree(p.parent, ignore_errors=True)
128
+
129
+ def test_a_join_alias_resolves_to_the_source_it_targets(self):
130
+ """`join_one: destination is airports` declares `destination`, so
131
+ `destination.airport_count` is a field of `airports`.
132
+
133
+ Scoping bare words to the declaring source is right; treating a join
134
+ ALIAS as if it were a source name is not, and it dropped the edge
135
+ outright. Measured on faa: `flights.destination_count` came back
136
+ depending on nothing, so an edit to `airports.airport_count` would not
137
+ have moved its sha. The join is a dependency too, because its `on`
138
+ clause decides which rows the field sees.
139
+ """
140
+ p = write(
141
+ "source: airports is duckdb.table('data/a.parquet') extend {\n"
142
+ " measure:\n airport_count is count()\n}\n"
143
+ "source: flights is duckdb.table('data/f.parquet') extend {\n"
144
+ " join_one: destination is airports with destination_code\n"
145
+ " measure:\n destination_count is destination.airport_count\n}\n")
146
+ try:
147
+ recs = {r["entityId"]: r for r in vd.records(p, recursive=False)}
148
+ self.assertEqual(
149
+ recs["measure:flights:destination_count"]["depends"],
150
+ ["measure:airports:airport_count", "join:flights:destination"])
151
+ finally:
152
+ shutil.rmtree(p.parent, ignore_errors=True)
153
+
154
+ def test_a_path_two_joins_deep_reaches_its_last_segment(self):
155
+ """`re.findall` does not overlap, so a pairwise `(\\w+)\\.(\\w+)` reads
156
+ `a.b.c` as the single match `(a, b)` and never sees `c`.
157
+
158
+ The dependency at the END of the path is the one the sha chain exists
159
+ to track: with it dropped, editing the leaf measure left the measure
160
+ two joins above it reading as current, and `verify_goldens
161
+ --definitions` would promote against a definition that had moved.
162
+ """
163
+ p = write(
164
+ "source: leaf is duckdb.table('data/l.parquet') extend {\n"
165
+ " measure:\n leaf_measure is count()\n}\n"
166
+ "source: mid is duckdb.table('data/m.parquet') extend {\n"
167
+ " join_one: leaf_alias is leaf with leaf_id\n"
168
+ " measure:\n mid_measure is leaf_alias.leaf_measure\n}\n"
169
+ "source: top is duckdb.table('data/t.parquet') extend {\n"
170
+ " join_one: mid_alias is mid with mid_id\n"
171
+ " measure:\n"
172
+ " top_measure is mid_alias.leaf_alias.leaf_measure\n}\n")
173
+ try:
174
+ recs = {r["entityId"]: r for r in vd.records(p, recursive=False)}
175
+ self.assertIn("measure:leaf:leaf_measure",
176
+ recs["measure:top:top_measure"]["depends"])
177
+ # Both joins on the way are dependencies too: either one's `on`
178
+ # clause changes which rows the leaf measure is read over.
179
+ self.assertIn("join:top:mid_alias",
180
+ recs["measure:top:top_measure"]["depends"])
181
+ self.assertIn("join:mid:leaf_alias",
182
+ recs["measure:top:top_measure"]["depends"])
183
+ finally:
184
+ shutil.rmtree(p.parent, ignore_errors=True)
185
+
186
+ def test_editing_a_leaf_moves_a_measure_two_joins_above_it(self):
187
+ base = ("source: leaf is duckdb.table('data/l.parquet') extend {\n"
188
+ " measure:\n leaf_measure is count()\n}\n"
189
+ "source: mid is duckdb.table('data/m.parquet') extend {\n"
190
+ " join_one: leaf_alias is leaf with leaf_id\n"
191
+ " measure:\n mid_measure is leaf_alias.leaf_measure\n}\n"
192
+ "source: top is duckdb.table('data/t.parquet') extend {\n"
193
+ " join_one: mid_alias is mid with mid_id\n"
194
+ " measure:\n"
195
+ " top_measure is mid_alias.leaf_alias.leaf_measure\n}\n")
196
+
197
+ def shas(text):
198
+ p = write(text)
199
+ try:
200
+ return {r["entityId"]: r["exprSha"]
201
+ for r in vd.records(p, recursive=False)}
202
+ finally:
203
+ shutil.rmtree(p.parent, ignore_errors=True)
204
+
205
+ before = shas(base)
206
+ after = shas(base.replace("leaf_measure is count()",
207
+ "leaf_measure is count() * 2"))
208
+ self.assertNotEqual(before["measure:top:top_measure"],
209
+ after["measure:top:top_measure"])
210
+
211
+ def test_a_method_call_on_a_column_is_not_a_traversal(self):
212
+ # `sale_price.sum()` is a path too; the walk must stop at a segment
213
+ # that names no join and no source, or every method call invents an
214
+ # edge.
215
+ p = write("source: s is duckdb.table('data/s.parquet') extend {\n"
216
+ " measure:\n total is sale_price.sum()\n}\n")
217
+ try:
218
+ recs = {r["entityId"]: r for r in vd.records(p, recursive=False)}
219
+ self.assertEqual(recs["measure:s:total"]["depends"], [])
220
+ finally:
221
+ shutil.rmtree(p.parent, ignore_errors=True)
222
+
223
+ def test_the_two_same_named_measures_hash_apart(self):
224
+ a = self.recs["measure:source_a:total_sales"]["exprSha"]
225
+ b = self.recs["measure:source_b:total_sales"]["exprSha"]
226
+ self.assertNotEqual(a, b)
227
+
228
+
229
+ class AnEntityIdAddressesExactlyOneDefinition(unittest.TestCase):
230
+ """`load_ledger` and `stale_ids` both build `{entityId: record}`, so two
231
+ definitions sharing an id means one is never staleness-checked and an
232
+ authored control written for one re-runs against the other. Measured on
233
+ real packages before this refused: auto_recalls lost 12 of 24 rows, faa 1
234
+ of 36."""
235
+
236
+ def test_two_files_declaring_one_source_name_are_refused(self):
237
+ d = pathlib.Path(tempfile.mkdtemp())
238
+ try:
239
+ (d / "a.malloy").write_text(
240
+ "source: airports is duckdb.table('data/a.parquet') extend {\n"
241
+ " measure:\n airport_count is count()\n}\n")
242
+ (d / "b.malloy").write_text(
243
+ "source: airports is duckdb.table('data/a.parquet') extend {\n"
244
+ " measure:\n airport_count is count()\n}\n")
245
+ with self.assertRaises(vd.Collision) as caught:
246
+ vd.records(d, recursive=False)
247
+ msg = str(caught.exception)
248
+ self.assertIn("measure:airports:airport_count", msg)
249
+ # Both sites named, so the reader can go fix one of them.
250
+ self.assertIn("a.malloy", msg)
251
+ self.assertIn("b.malloy", msg)
252
+ self.assertIn("Nothing was written", msg)
253
+ finally:
254
+ shutil.rmtree(d, ignore_errors=True)
255
+
256
+ def test_one_file_declaring_two_sources_is_fine(self):
257
+ p = write(TWO_SOURCES)
258
+ try:
259
+ self.assertEqual(len(vd.records(p, recursive=False)), 4)
260
+ finally:
261
+ shutil.rmtree(p.parent, ignore_errors=True)
262
+
263
+ def test_a_field_outside_any_source_is_not_a_ledger_entity(self):
264
+ # An aggregate inside a `run:` block. It cannot be required by a case,
265
+ # because a required id is `kind:source:name` and there is no source to
266
+ # name, and `entity_id` would mint a two-part id that `check_findable`
267
+ # reports as malformed.
268
+ p = write("source: s is duckdb.table('data/s.parquet') extend {\n"
269
+ " measure:\n real_one is count()\n}\n"
270
+ "run: s -> {\n aggregate:\n query_local is count()\n}\n")
271
+ try:
272
+ recs = vd.records(p, recursive=False)
273
+ ids = [r["entityId"] for r in recs]
274
+ self.assertIn("measure:s:real_one", ids)
275
+ self.assertTrue(all(i.count(":") == 2 for i in ids), ids)
276
+ self.assertNotIn("measure:query_local", ids)
277
+ finally:
278
+ shutil.rmtree(p.parent, ignore_errors=True)
279
+
280
+
281
+ class TheShaChainInvalidatesDownstream(unittest.TestCase):
282
+ """A measure whose own text never moved is still stale when something it
283
+ builds on does. A flat hash of the one line reports it as current, which is
284
+ how a ledger goes quietly wrong."""
285
+
286
+ def shas(self, text):
287
+ p = write(text)
288
+ try:
289
+ return {r["name"]: r["exprSha"] for r in vd.records(p, False)}
290
+ finally:
291
+ shutil.rmtree(p.parent, ignore_errors=True)
292
+
293
+ def test_editing_a_dependency_moves_its_dependant(self):
294
+ before = self.shas(MODEL)
295
+ after = self.shas(MODEL.replace("total_sales is sale_price.sum()",
296
+ "total_sales is sale_price.sum() * 2"))
297
+ self.assertNotEqual(before["total_sales"], after["total_sales"])
298
+ # The point: this one's own text is byte-identical across the two.
299
+ self.assertNotEqual(before["total_sales_2022"], after["total_sales_2022"])
300
+
301
+ def test_an_unrelated_definition_is_not_disturbed(self):
302
+ before = self.shas(MODEL)
303
+ after = self.shas(MODEL.replace("total_sales is sale_price.sum()",
304
+ "total_sales is sale_price.sum() * 2"))
305
+ self.assertEqual(before["order_item_count"], after["order_item_count"])
306
+
307
+
308
+ class WhatItRefusesToCheck(unittest.TestCase):
309
+ def setUp(self):
310
+ self.model = write(MODEL)
311
+ self.recs = by_name(vd.records(self.model, recursive=False))
312
+
313
+ def tearDown(self):
314
+ shutil.rmtree(self.model.parent, ignore_errors=True)
315
+
316
+ def test_a_join_crossing_measure_is_held_back_for_a_raw_check(self):
317
+ # Fanout inflates the measure and the control expression equally, so a
318
+ # within-model comparison stays green on a genuinely broken join.
319
+ r = self.recs["total_cost_through_join"]
320
+ self.assertEqual(r["check"]["kind"], "raw")
321
+ self.assertIn("inventory_items", r["needs"])
322
+
323
+ def test_a_method_call_on_a_column_is_not_a_join(self):
324
+ # The regression: matching any `word.word` read `sale_price.sum()` as a
325
+ # join hop and held back the measure twelve of the set's cases use.
326
+ self.assertEqual(self.recs["total_sales"]["check"]["kind"], "within_model")
327
+
328
+ def test_a_fanout_safe_aggregate_across_a_join_is_still_checkable(self):
329
+ # MIN survives uniform duplication, so fanout cannot move it.
330
+ self.assertEqual(self.recs["worst_margin"]["check"]["kind"], "within_model")
331
+
332
+ def test_a_multi_line_definition_is_unreadable_not_checked(self):
333
+ p = write("source: s is t extend {\n dimension:\n full_name is concat(\n")
334
+ try:
335
+ r = by_name(vd.records(p, False))["full_name"]
336
+ self.assertEqual(r["check"]["kind"], "unreadable")
337
+ self.assertIn("more than one line", r["needs"])
338
+ finally:
339
+ shutil.rmtree(p.parent, ignore_errors=True)
340
+
341
+ def test_incomplete_spots_every_unbalanced_delimiter(self):
342
+ for expr in ("concat(", "x { where: y", "arr[0"):
343
+ self.assertTrue(vd.incomplete(expr), expr)
344
+ for expr in ("sale_price.sum()", "count() { where: s != 'C' }"):
345
+ self.assertFalse(vd.incomplete(expr), expr)
346
+
347
+
348
+ class RunningOneCheck(unittest.TestCase):
349
+ def ns(self):
350
+ return argparse.Namespace(publisher="http://t", environment="e",
351
+ package="p", model_path="m.malloy")
352
+
353
+ def rec(self, **kw):
354
+ base = {"kind": "measure", "source": "order_items", "name": "total_sales",
355
+ "expr": "sale_price.sum()", "check": {"kind": "within_model"},
356
+ "needs": None}
357
+ base.update(kw)
358
+ return base
359
+
360
+ def check(self, rows, err=None, **kw):
361
+ with mock.patch.object(vd, "try_query", lambda *a, **k: (rows, err)):
362
+ return vd.run_check(self.rec(**kw), self.ns())
363
+
364
+ def test_matching_values_agree(self):
365
+ v, _ = self.check([{"stated": 42.0, "control": 42.0}])
366
+ self.assertEqual(v, "agrees")
367
+
368
+ def test_a_different_value_disagrees_and_names_both(self):
369
+ v, detail = self.check([{"stated": 84.0, "control": 42.0}])
370
+ self.assertEqual(v, "disagrees")
371
+ self.assertIn("84", detail)
372
+ self.assertIn("42", detail)
373
+
374
+ def test_float_association_is_not_a_disagreement(self):
375
+ # Two aggregation orders differ in the last places; verify_goldens
376
+ # tolerates the same.
377
+ v, _ = self.check([{"stated": 5788744.332576918,
378
+ "control": 5788744.3325769575}])
379
+ self.assertEqual(v, "agrees")
380
+
381
+ def test_a_failed_query_is_unchecked_not_a_disagreement(self):
382
+ # A check that could not run is not a check that found a defect.
383
+ v, detail = self.check([], err="HTTP 400: nope")
384
+ self.assertEqual(v, "unchecked")
385
+ self.assertIn("query failed", detail)
386
+
387
+ def test_no_rows_is_unchecked(self):
388
+ v, _ = self.check([])
389
+ self.assertEqual(v, "unchecked")
390
+
391
+ def test_a_raw_check_is_never_run_here(self):
392
+ v, _ = self.check([{"stated": 1, "control": 2}],
393
+ check={"kind": "raw"}, needs="crosses a join")
394
+ self.assertEqual(v, "unchecked")
395
+
396
+ def test_a_dimension_compares_every_sampled_row(self):
397
+ rows = [{"stated": i, "control": i} for i in range(5)]
398
+ rows[3]["control"] = 99
399
+ v, detail = self.check(rows, kind="dimension", name="order_year",
400
+ expr="year(created_at)")
401
+ self.assertEqual(v, "disagrees")
402
+ self.assertIn("row 4", detail)
403
+
404
+ def test_an_all_null_slice_is_unchecked_not_agreed(self):
405
+ # Every sampled row null on both sides proves nothing about the
406
+ # definition; calling it a pass is the over-claim this file guards.
407
+ v, detail = self.check([{"stated": None, "control": None}] * 3,
408
+ kind="dimension")
409
+ self.assertEqual(v, "unchecked")
410
+ self.assertIn("null", detail)
411
+
412
+ def test_a_passing_dimension_records_the_slice_it_used(self):
413
+ rec = self.rec(kind="dimension")
414
+ with mock.patch.object(vd, "try_query",
415
+ lambda *a, **k: ([{"stated": 1, "control": 1}] * 7, None)):
416
+ v, _ = vd.run_check(rec, self.ns())
417
+ self.assertEqual(v, "agrees")
418
+ self.assertIn("slice", rec["check"])
419
+
420
+
421
+ class TheQueryItBuilds(unittest.TestCase):
422
+ def test_a_measure_aggregates(self):
423
+ q = vd.within_model_query({"kind": "measure", "source": "s",
424
+ "name": "m", "expr": "x.sum()"})
425
+ self.assertIn("aggregate:", q)
426
+ self.assertNotIn("limit:", q)
427
+
428
+ def test_a_dimension_selects_a_slice(self):
429
+ # `aggregate:` rejects a scalar outright ("Cannot use a scalar field in
430
+ # an aggregate"), which is how this was found.
431
+ q = vd.within_model_query({"kind": "dimension", "source": "s",
432
+ "name": "d", "expr": "year(t)"})
433
+ self.assertIn("select:", q)
434
+ self.assertIn(f"limit: {vd.DIM_SLICE}", q)
435
+
436
+ def test_both_sides_come_from_one_query(self):
437
+ # Two queries could see different rows, and the difference would read
438
+ # as a bad definition rather than a moving population.
439
+ q = vd.within_model_query({"kind": "measure", "source": "s",
440
+ "name": "m", "expr": "x.sum()"})
441
+ self.assertEqual(q.count("run:"), 1)
442
+
443
+
444
+ class TheExitContract(unittest.TestCase):
445
+ """Matches verify_goldens: 0 clean, 1 a finding, 3 could not run."""
446
+
447
+ def setUp(self):
448
+ self.model = write(MODEL)
449
+
450
+ def tearDown(self):
451
+ shutil.rmtree(self.model.parent, ignore_errors=True)
452
+
453
+ def run_main(self, rows, extra=()):
454
+ argv = ["--model", str(self.model), "--publisher", "http://t",
455
+ "--package", "p", "--model-path", "m.malloy", "--quiet", *extra]
456
+ with mock.patch.object(vd, "try_query", lambda *a, **k: (rows, None)):
457
+ return vd.main(argv)
458
+
459
+ def test_a_disagreement_exits_1(self):
460
+ self.assertEqual(self.run_main([{"stated": 1, "control": 2}]), 1)
461
+
462
+ def test_agreement_exits_0(self):
463
+ self.assertEqual(self.run_main([{"stated": 1, "control": 1}]), 0)
464
+
465
+ def test_building_without_a_server_is_could_not_run_not_a_pass(self):
466
+ # The whole point of 3: a ledger built and never checked must not read
467
+ # as validation.
468
+ self.assertEqual(vd.main(["--model", str(self.model), "--quiet"]),
469
+ vd.CANNOT_RUN)
470
+
471
+ def test_a_missing_model_is_could_not_run(self):
472
+ self.assertEqual(vd.main(["--model", "/nope/absent.malloy", "--quiet"]),
473
+ vd.CANNOT_RUN)
474
+
475
+ def test_a_publisher_without_an_address_is_could_not_run(self):
476
+ self.assertEqual(vd.main(["--model", str(self.model),
477
+ "--publisher", "http://t", "--quiet"]),
478
+ vd.CANNOT_RUN)
479
+
480
+ def test_ledger_mode_reads_merges_checks_and_writes_back(self):
481
+ led = self.model.parent / "led.jsonl"
482
+ vd.main(["--model", str(self.model), "--out", str(led), "--quiet"])
483
+ rows = [json.loads(l) for l in led.read_text().splitlines()]
484
+ for r in rows:
485
+ if r["name"] == "total_cost_through_join":
486
+ r["check"]["query"] = "run: x -> { aggregate: control is 1 }"
487
+ led.write_text("".join(json.dumps(r) + "\n" for r in rows))
488
+ with mock.patch.object(vd, "try_query",
489
+ lambda *a, **k: ([{"stated": 1, "control": 1}], None)):
490
+ code = vd.main(["--model", str(self.model), "--ledger", str(led),
491
+ "--publisher", "http://t", "--package", "p",
492
+ "--model-path", "m.malloy", "--quiet"])
493
+ self.assertEqual(code, 0)
494
+ back = {r["name"]: r for r in
495
+ (json.loads(l) for l in led.read_text().splitlines())}
496
+ self.assertEqual(back["total_cost_through_join"]["check"]["query"],
497
+ "run: x -> { aggregate: control is 1 }")
498
+ self.assertEqual(back["total_cost_through_join"]["verdict"], "agrees")
499
+
500
+ def test_a_missing_ledger_is_could_not_run(self):
501
+ self.assertEqual(vd.main(["--model", str(self.model),
502
+ "--ledger", "/nope/led.jsonl", "--quiet"]),
503
+ vd.CANNOT_RUN)
504
+
505
+ def test_the_ledger_is_written_as_jsonl(self):
506
+ out = self.model.parent / "led.jsonl"
507
+ self.run_main([{"stated": 1, "control": 1}], extra=["--out", str(out)])
508
+ rows = [json.loads(l) for l in out.read_text().splitlines() if l.strip()]
509
+ self.assertTrue(rows)
510
+ self.assertTrue(all("entityId" in r and "exprSha" in r for r in rows))
511
+
512
+
513
+ class WhatTheScoreRestsOn(unittest.TestCase):
514
+ """The composition rule, per case: a golden is trustworthy if it was derived
515
+ independently, OR if every definition it tests has been validated."""
516
+
517
+ def ledger(self, **verdicts):
518
+ return {eid: {"entityId": eid, "verdict": v, "exprSha": "s"}
519
+ for eid, v in verdicts.items()}
520
+
521
+ def case(self, ids=("measure:s:m",), **golden):
522
+ g = {"status": "verified", "kind": "scalar", "value": 1}
523
+ g.update(golden)
524
+ return {"qid": "q", "golden": g,
525
+ "expectedEntities": {"required": list(ids)}}
526
+
527
+ def setUp(self):
528
+ self.set_dir = pathlib.Path(tempfile.mkdtemp())
529
+
530
+ def tearDown(self):
531
+ shutil.rmtree(self.set_dir, ignore_errors=True)
532
+
533
+ def basis(self, case, ledger=None, stale=()):
534
+ return vd.case_basis(case, ledger or {}, set(stale), self.set_dir)
535
+
536
+ def test_a_second_derivation_makes_a_key_independent(self):
537
+ # It was established twice, in differently shaped ways, so no single
538
+ # model bug could have certified it.
539
+ c = self.case()
540
+ c["golden"]["verification"] = {"primaryAxis": "a", "variesAxis": "b"}
541
+ self.assertEqual(self.basis(c), "independent")
542
+
543
+ def test_free_text_verifiedby_is_not_the_signal(self):
544
+ # The regression: matching `verifiedBy` by prefix classified 34 goldens
545
+ # of the ecommerce set as unchecked, every one of which reads "authored
546
+ # and re-derived against ecommerce-truth". A well-founded set reading as
547
+ # unvalidated is the same over-claim, pointed the other way.
548
+ c = self.case(verifiedBy="authored and re-derived against ecommerce-truth")
549
+ self.assertEqual(self.basis(c, self.ledger()), "unchecked")
550
+ c["golden"]["verification"] = {"primaryAxis": "a", "variesAxis": "b"}
551
+ self.assertEqual(self.basis(c, self.ledger()), "independent")
552
+
553
+ def test_a_value_free_golden_is_independent(self):
554
+ # Nothing to re-derive, so nothing a model bug could get wrong.
555
+ for kind in ("criteria", "unanswerable"):
556
+ self.assertEqual(self.basis(self.case(kind=kind)), "independent")
557
+
558
+ def test_validated_definitions_carry_a_model_derived_key(self):
559
+ self.assertEqual(
560
+ self.basis(self.case(verifiedBy="something else"),
561
+ self.ledger(**{"measure:s:m": "agrees"})),
562
+ "definitions")
563
+
564
+ def test_one_unchecked_definition_makes_the_whole_case_unchecked(self):
565
+ # The weakest link decides. A case is only as established as the least
566
+ # established thing it depends on.
567
+ led = self.ledger(**{"measure:s:m": "agrees", "measure:s:n": "unchecked"})
568
+ self.assertEqual(
569
+ self.basis(self.case(ids=("measure:s:m", "measure:s:n")), led),
570
+ "unchecked")
571
+
572
+ def test_a_definition_missing_from_the_ledger_is_unchecked(self):
573
+ self.assertEqual(self.basis(self.case(), self.ledger()), "unchecked")
574
+
575
+ def test_a_stale_row_does_not_count_as_validated(self):
576
+ # It agreed once, against a definition that has since moved.
577
+ led = self.ledger(**{"measure:s:m": "agrees"})
578
+ self.assertEqual(self.basis(self.case(), led, stale=("measure:s:m",)),
579
+ "unchecked")
580
+
581
+ def test_a_disagreeing_definition_outranks_everything(self):
582
+ led = self.ledger(**{"measure:s:m": "disagrees"})
583
+ self.assertEqual(self.basis(self.case(), led), "disagrees")
584
+
585
+ def test_a_case_naming_no_entities_is_unchecked_not_validated(self):
586
+ # Nothing was established, so it must not read as though something was.
587
+ c = {"qid": "q", "golden": {"kind": "scalar", "value": 1}}
588
+ self.assertEqual(self.basis(c, self.ledger()), "unchecked")
589
+
590
+ def test_an_anyof_group_is_satisfied_by_either_member(self):
591
+ # The regression: the groups were flattened and every member demanded,
592
+ # so a case answerable through `startYear` OR the model's alias for it
593
+ # was held unvalidated because only the alias is a checkable definition.
594
+ c = {"qid": "q", "golden": {"kind": "scalar", "value": 1},
595
+ "expectedEntities": {"requiredAnyOf": [["dimension:s:raw",
596
+ "dimension:s:alias"]]}}
597
+ led = {"dimension:s:alias": {"verdict": "agrees", "exprSha": "x"}}
598
+ self.assertEqual(vd.case_basis(c, led, set(), self.set_dir), "definitions")
599
+
600
+ def test_a_passthrough_column_counts_as_validated(self):
601
+ # A raw column exposed as-is has no expression that could be wrong.
602
+ # Absent from the ledger it blocked a case forever; recorded as
603
+ # `no_definition` it is what it is.
604
+ c = {"qid": "q", "golden": {"kind": "scalar", "value": 1},
605
+ "expectedEntities": {"required": ["dimension:s:runtimeMinutes"]}}
606
+ led = {"dimension:s:runtimeMinutes": {"verdict": "no_definition",
607
+ "exprSha": None}}
608
+ self.assertEqual(vd.case_basis(c, led, set(), self.set_dir), "definitions")
609
+
610
+ def test_an_anyof_group_with_no_validated_member_is_unchecked(self):
611
+ c = {"qid": "q", "golden": {"kind": "scalar", "value": 1},
612
+ "expectedEntities": {"requiredAnyOf": [["dimension:s:a",
613
+ "dimension:s:b"]]}}
614
+ self.assertEqual(vd.case_basis(c, {}, set(), self.set_dir), "unchecked")
615
+
616
+ def test_requiredanyof_members_count_as_tested(self):
617
+ c = {"qid": "q", "golden": {"kind": "scalar", "value": 1},
618
+ "expectedEntities": {"requiredAnyOf": [["measure:s:a", "measure:s:b"]]}}
619
+ self.assertEqual(vd.tested_ids(c), ["measure:s:a", "measure:s:b"])
620
+
621
+ def test_a_disagreeing_definition_is_named_even_under_an_independent_key(self):
622
+ # The golden was derived from raw tables and is trustworthy; the model's
623
+ # own total_sales still contradicts its docs. Both are true, and the
624
+ # second is the finding the run exists to surface.
625
+ c = self.case()
626
+ c["golden"]["verification"] = {"primaryAxis": "a", "variesAxis": "b"}
627
+ ev = vd.evidence_basis([c], self.ledger(**{"measure:s:m": "disagrees"}),
628
+ set(), self.set_dir)
629
+ self.assertEqual(ev["counts"], {"independent": 1})
630
+ self.assertEqual(ev["disagreeing"], ["measure:s:m"])
631
+
632
+ def test_the_basis_totals_cover_every_case(self):
633
+ cases = [self.case(kind="criteria"), self.case(), self.case()]
634
+ ev = vd.evidence_basis(cases, self.ledger(), set(), self.set_dir)
635
+ self.assertEqual(sum(ev["counts"].values()), len(cases))
636
+
637
+
638
+ class StalenessIsAHashComparison(unittest.TestCase):
639
+ def setUp(self):
640
+ self.model = write(MODEL)
641
+
642
+ def tearDown(self):
643
+ shutil.rmtree(self.model.parent, ignore_errors=True)
644
+
645
+ def test_an_unmoved_definition_is_not_stale(self):
646
+ led = {r["entityId"]: r for r in vd.records(self.model, False)}
647
+ self.assertEqual(vd.stale_ids(led, self.model), set())
648
+
649
+ def test_editing_a_dependency_makes_its_dependant_stale_too(self):
650
+ led = {r["entityId"]: r for r in vd.records(self.model, False)}
651
+ self.model.write_text(MODEL.replace("total_sales is sale_price.sum()",
652
+ "total_sales is sale_price.sum() * 2"))
653
+ stale = vd.stale_ids(led, self.model)
654
+ self.assertIn("measure:order_items:total_sales", stale)
655
+ # Its own text never changed; the sha chain is what catches it.
656
+ self.assertIn("measure:order_items:total_sales_2022", stale)
657
+ self.assertNotIn("measure:order_items:order_item_count", stale)
658
+
659
+ def test_a_removed_definition_is_stale(self):
660
+ led = {"measure:order_items:gone": {"entityId": "measure:order_items:gone",
661
+ "exprSha": "x"}}
662
+ self.assertEqual(vd.stale_ids(led, self.model),
663
+ {"measure:order_items:gone"})
664
+
665
+ def test_no_model_means_nothing_can_be_called_stale(self):
666
+ self.assertEqual(vd.stale_ids({"a": {"exprSha": "x"}}, None), set())
667
+
668
+
669
+ class AuthoredControls(unittest.TestCase):
670
+ """A raw check is a control query a person wrote into the ledger, re-run and
671
+ compared here. Re-deriving the definition's own expression over raw tables
672
+ computes the same quantity and agrees; what catches a wrong population is
673
+ the person who read the docs and wrote the population down."""
674
+
675
+ def ns(self, truth=None):
676
+ return argparse.Namespace(
677
+ publisher="http://m", environment="e", package="p",
678
+ model_path="m.malloy", truth_publisher=truth, truth_environment=None,
679
+ truth_package="truth", truth_model="truth.malloy")
680
+
681
+ def rec(self, **check):
682
+ c = {"kind": "raw"}
683
+ c.update(check)
684
+ return {"kind": "measure", "source": "order_items",
685
+ "name": "total_gross_margin",
686
+ "expr": "sale_price.sum() - inventory_items.cost.sum()",
687
+ "check": c, "needs": "reaches through inventory_items"}
688
+
689
+ def exercise(self, rec, stated, control, truth=None):
690
+ calls = []
691
+
692
+ def fake(base, env, pkg, model, q, **k):
693
+ calls.append(base)
694
+ if "stated is" in q:
695
+ return [{"stated": stated}], None
696
+ return [{"control": control}], None
697
+ with mock.patch.object(vd, "try_query", fake):
698
+ v, d = vd.run_check(rec, self.ns(truth))
699
+ return v, d, calls
700
+
701
+ def test_no_authored_query_stays_unchecked(self):
702
+ v, d, _ = self.exercise(self.rec(), 1, 1)
703
+ self.assertEqual(v, "unchecked")
704
+ self.assertIn("check.query", d)
705
+
706
+ def test_an_agreeing_control_agrees(self):
707
+ v, _, _ = self.exercise(self.rec(query="run: x -> { aggregate: control is y }"),
708
+ 5.0, 5.0)
709
+ self.assertEqual(v, "agrees")
710
+
711
+ def test_a_disagreeing_control_disagrees_and_carries_the_note(self):
712
+ # The cogs shape: the model includes cancelled lines its own status doc
713
+ # says are not sales. Nothing mechanical sees that; the person who read
714
+ # the doc did, and wrote it down.
715
+ rec = self.rec(query="run: x -> { aggregate: control is z }",
716
+ note="excludes Cancelled, per the status doc")
717
+ v, d, _ = self.exercise(rec, 6002288.38, 5788744.33)
718
+ self.assertEqual(v, "disagrees")
719
+ self.assertIn("status doc", d)
720
+
721
+ def test_against_truth_routes_only_the_control_to_the_truth_server(self):
722
+ rec = self.rec(query="run: raw -> { aggregate: control is c }",
723
+ against="truth")
724
+ v, _, calls = self.exercise(rec, 1, 1, truth="http://t")
725
+ self.assertEqual(v, "agrees")
726
+ # stated came from the model server, the control from the truth server
727
+ self.assertEqual(sorted(set(calls)), ["http://m", "http://t"])
728
+
729
+ def test_against_truth_without_a_truth_server_is_unchecked(self):
730
+ rec = self.rec(query="run: raw -> {}", against="truth")
731
+ v, d, _ = self.exercise(rec, 1, 1)
732
+ self.assertEqual(v, "unchecked")
733
+ self.assertIn("--truth-publisher", d)
734
+
735
+ def test_an_unknown_target_is_unchecked(self):
736
+ v, d, _ = self.exercise(self.rec(query="run: x -> {}", against="warehouse"), 1, 1)
737
+ self.assertEqual(v, "unchecked")
738
+ self.assertIn("warehouse", d)
739
+
740
+ def test_a_control_without_a_control_column_is_unchecked_not_a_crash(self):
741
+ def fake(base, env, pkg, model, q, **k):
742
+ return ([{"stated": 1}] if "stated is" in q else [{"wrong": 1}]), None
743
+ with mock.patch.object(vd, "try_query", fake):
744
+ v, d = vd.run_check(self.rec(query="run: x -> {}"), self.ns())
745
+ self.assertEqual(v, "unchecked")
746
+ self.assertIn("`control`", d)
747
+
748
+ def test_a_dimension_is_not_a_target_for_an_authored_control(self):
749
+ rec = self.rec(query="run: x -> {}")
750
+ rec["kind"] = "dimension"
751
+ v, _, _ = self.exercise(rec, 1, 1)
752
+ self.assertEqual(v, "unchecked")
753
+
754
+ def test_an_authored_control_supersedes_the_within_model_check(self):
755
+ # total_sales is sale_price.sum() agrees with itself while including the
756
+ # cancelled lines its own docs exclude. The within-model check cannot see
757
+ # that; a person's population assertion can, and it wins.
758
+ rec = self.rec(kind="within_model",
759
+ query="run: order_items -> { aggregate: control is "
760
+ "sale_price.sum() { where: status != 'Cancelled' } }",
761
+ note="excludes Cancelled, per the status doc")
762
+ rec["name"], rec["expr"] = "total_sales", "sale_price.sum()"
763
+ v, d, _ = self.exercise(rec, 12566292.88, 12119909.43)
764
+ self.assertEqual(v, "disagrees")
765
+ self.assertIn("authored control", d)
766
+
767
+ def test_a_failing_control_query_is_unchecked_not_a_disagreement(self):
768
+ def fake(base, env, pkg, model, q, **k):
769
+ if "stated is" in q:
770
+ return [{"stated": 1}], None
771
+ return [], "HTTP 400: nope"
772
+ with mock.patch.object(vd, "try_query", fake):
773
+ v, d = vd.run_check(self.rec(query="run: x -> {}"), self.ns())
774
+ self.assertEqual(v, "unchecked")
775
+ self.assertIn("control query failed", d)
776
+
777
+
778
+ class TheLedgerKeepsWhatAPersonWrote(unittest.TestCase):
779
+ """`--ledger` rebuilds from the model and must not lose an authored control,
780
+ and must not inherit a verdict from a definition that may have moved."""
781
+
782
+ def setUp(self):
783
+ self.model = write(MODEL)
784
+
785
+ def tearDown(self):
786
+ shutil.rmtree(self.model.parent, ignore_errors=True)
787
+
788
+ def test_a_rebuild_carries_authored_fields_forward_and_recomputes_the_rest(self):
789
+ eid = "measure:order_items:total_cost_through_join"
790
+ prior = {r["entityId"]: r for r in vd.records(self.model, False)}
791
+ prior[eid]["check"]["query"] = "run: x -> { aggregate: control is 1 }"
792
+ prior[eid]["check"]["note"] = "why"
793
+ prior[eid]["cause"] = "CONVENTION"
794
+ prior[eid]["verdict"] = "agrees" # must NOT survive
795
+ second = {r["entityId"]: r
796
+ for r in vd.records(self.model, False, existing=prior)}
797
+ self.assertEqual(second[eid]["check"]["query"],
798
+ "run: x -> { aggregate: control is 1 }")
799
+ self.assertEqual(second[eid]["check"]["note"], "why")
800
+ self.assertEqual(second[eid]["cause"], "CONVENTION")
801
+ self.assertEqual(second[eid]["verdict"], "unchecked")
802
+
803
+ def test_a_definition_the_model_no_longer_declares_is_dropped(self):
804
+ gone = "measure:order_items:gone"
805
+ prior = {gone: {"entityId": gone, "check": {"query": "q"}}}
806
+ ids = {r["entityId"] for r in vd.records(self.model, False, existing=prior)}
807
+ self.assertNotIn(gone, ids)
808
+
809
+
810
+ class TheSummary(unittest.TestCase):
811
+ def test_it_says_how_many_are_not_validated_by_this_run(self):
812
+ recs = [{"verdict": "agrees", "check": {"kind": "within_model"}},
813
+ {"verdict": "unchecked", "check": {"kind": "raw"}}]
814
+ text = " ".join(vd.summarise(recs))
815
+ self.assertIn("1 agrees", text)
816
+ self.assertIn("NOT validated", text)
817
+
818
+
819
+ if __name__ == "__main__":
820
+ unittest.main(verbosity=1)