@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
@@ -6,11 +6,12 @@ regression they exist for: an extractor written against the flat `results`
6
6
  shape returned [] for the nested one, which scored attempts that had received
7
7
  everything at 0% recall and moved three baseline failures off "query
8
8
  construction" onto "retrieval ranking" and "model coverage" -- the wrong team,
9
- for a bug in the eval.
9
+ for a bug in the eval. Those are the labels as they read then; the first two
10
+ are now `delivered, wrong` and `not retrieved`.
10
11
  """
11
12
  import unittest
12
13
 
13
- from mcp_payload import entity_ids, search_terms
14
+ from mcp_payload import entity_hits, entity_ids, search_terms
14
15
 
15
16
  FLAT = {"results": [
16
17
  {"kind": "measure", "name": "total_sales_2022", "source": "order_items",
@@ -215,5 +216,75 @@ class EntityIds(unittest.TestCase):
215
216
  ["measure:s:m"])
216
217
 
217
218
 
219
+ class EntityHits(unittest.TestCase):
220
+ """The provenance `entity_ids` throws away.
221
+
222
+ Without it, a required entity that never came back is indistinguishable
223
+ from one the agent never asked for -- a retrieval defect from a phrase
224
+ detection defect -- which is the reason retrieval scoring could not
225
+ attribute a miss.
226
+ """
227
+
228
+ PAYLOAD = {"sources": [{
229
+ "source_info": {"resource_id": {"source": "flights"}},
230
+ "entities": [
231
+ {"entity_id": "measure:flights:flight_count", "relevance": 0.81,
232
+ "matched_targets": [{"search_text": "flight count",
233
+ "relevance": 0.81}]},
234
+ {"name": "carrier", "entity_type": "dimension", "relevance": 0.44,
235
+ "matched_targets": [{"search_text": "carrier", "relevance": 0.44}]},
236
+ ]}]}
237
+
238
+ def test_it_agrees_with_entity_ids_about_identity(self):
239
+ # Two readers of one response that disagree about what an entity is
240
+ # called would make every comparison meaningless.
241
+ self.assertEqual([h["entity_id"] for h in entity_hits(self.PAYLOAD)],
242
+ entity_ids(self.PAYLOAD))
243
+
244
+ def test_it_carries_relevance_and_the_matching_targets(self):
245
+ h = {x["entity_id"]: x for x in entity_hits(self.PAYLOAD)}
246
+ m = h["measure:flights:flight_count"]
247
+ self.assertEqual(m["relevance"], 0.81)
248
+ self.assertEqual(m["matched_targets"],
249
+ [{"search_text": "flight count", "relevance": 0.81}])
250
+
251
+ def test_a_source_hit_has_no_attribution_and_says_null(self):
252
+ # The server discards a source card's target attribution before an
253
+ # entity object exists, so there is none to report. `null` is that
254
+ # claim; `[]` would claim the server attributed it to no target.
255
+ h = {x["entity_id"]: x for x in entity_hits(self.PAYLOAD)}
256
+ self.assertIsNone(h["source:flights:flights"]["matched_targets"])
257
+
258
+ def test_a_lexical_response_reports_no_attribution_not_an_empty_one(self):
259
+ # The lexical path withholds matched_targets by design: a lunr score is
260
+ # relative to its own query. Reading that as "matched no target" would
261
+ # charge the agent for never asking.
262
+ lex = {"sources": [{"source_info": {"resource_id": {"source": "f"}},
263
+ "entities": [{"entity_id": "measure:f:x"}]}]}
264
+ h = entity_hits(lex)[1]
265
+ self.assertIsNone(h["matched_targets"])
266
+ self.assertIsNone(h["relevance"])
267
+
268
+ def test_position_is_not_a_rank(self):
269
+ # Kept because it is occasionally useful for reading a response back,
270
+ # never because it orders by relevance: the response sorts globally
271
+ # and then buckets into source cards.
272
+ hits = entity_hits(self.PAYLOAD)
273
+ self.assertEqual([h["position"] for h in hits], [1, 2, 3])
274
+ self.assertEqual([h["position_in_source"] for h in hits], [0, 1, 2])
275
+
276
+ def test_a_duplicate_entity_is_taken_once(self):
277
+ dup = {"sources": [
278
+ {"source_info": {"resource_id": {"source": "f"}},
279
+ "entities": [{"entity_id": "measure:f:x", "relevance": 0.9}]},
280
+ {"source_info": {"resource_id": {"source": "g"}},
281
+ "entities": [{"entity_id": "measure:f:x", "relevance": 0.1}]}]}
282
+ ids = [h["entity_id"] for h in entity_hits(dup)]
283
+ self.assertEqual(ids.count("measure:f:x"), 1)
284
+
285
+ def test_an_empty_payload_is_no_hits(self):
286
+ self.assertEqual(entity_hits({}), [])
287
+
288
+
218
289
  if __name__ == "__main__":
219
290
  unittest.main(verbosity=2)
@@ -24,9 +24,15 @@ to the judge as the re-executed evidence for a verdict, so a nested result was
24
24
  judged against a golden it could not match.
25
25
 
26
26
  The recursive uncelling is therefore the one kept here, and both callers get it.
27
- It does not recover the field names INSIDE a nest -- the envelope only names the
28
- top level -- so nested records come back keyed `c0`, `c1`. That was true of the
29
- recursive version before this move too; it is a limit, not a regression.
27
+
28
+ The request also asks for `compactJson`, and that is what recovers the field
29
+ names INSIDE a nest. The typed envelope names only the top level, so a nested
30
+ record came back keyed `c0`, `c1` -- and a golden written the way its author
31
+ ran the query, with the real column names, then read as DRIFT, which blocks an
32
+ arm. That was called a limit here; it was not. Publisher has answered plain
33
+ rows all along, and the request simply never asked. `rows_from_result` handles
34
+ both shapes (compact answers with `result` as a JSON string), so the envelope
35
+ walk stays as the fallback for a server that ignores the flag.
30
36
 
31
37
  TWO ERROR CONVENTIONS, ON PURPOSE
32
38
 
@@ -103,7 +109,7 @@ def query(base: str, environment: str, package: str, model: str, malloy: str,
103
109
  """Run a Malloy query. Raises on transport, HTTP or parse failure."""
104
110
  req = urllib.request.Request(
105
111
  _query_url(base, environment, package, model),
106
- data=json.dumps({"query": malloy}).encode(),
112
+ data=json.dumps({"query": malloy, "compactJson": True}).encode(),
107
113
  headers={"Content-Type": "application/json"})
108
114
  with urllib.request.urlopen(req, timeout=timeout) as r:
109
115
  return rows_from_result(json.loads(r.read().decode()))
@@ -1,8 +1,11 @@
1
1
  #!/usr/bin/env python3
2
2
  """Tests for publisher_rest. Stdlib only: python3 publisher_rest_test.py"""
3
+ import json
3
4
  import os
4
5
  import unittest
6
+ import unittest.mock
5
7
 
8
+ import publisher_rest
6
9
  from publisher_rest import rows_from_result, uncell
7
10
 
8
11
 
@@ -73,6 +76,59 @@ class RowsFromResultTests(unittest.TestCase):
73
76
  [{"brand": "Levi's", "by_year": [{"c0": 2022}]}])
74
77
 
75
78
 
79
+ class CompactJson(unittest.TestCase):
80
+ """The request asks for compact rows, which is what recovers the names
81
+ inside a nest.
82
+
83
+ The typed envelope does not repeat field names below the top level, so a
84
+ nested record came back keyed `c0`, `c1`. A golden written the way its
85
+ author ran the query -- with the real column names -- then read as DRIFT,
86
+ and drift blocks an arm. Publisher has answered this all along; the
87
+ request just never asked.
88
+ """
89
+
90
+ def send(self, body):
91
+ """Capture the request `query()` builds, and hand back `body`."""
92
+ seen = {}
93
+
94
+ class Resp:
95
+ def read(self_): return json.dumps(body).encode()
96
+ def __enter__(self_): return self_
97
+ def __exit__(self_, *a): return False
98
+
99
+ def fake(req, timeout=None):
100
+ seen["payload"] = json.loads(req.data.decode())
101
+ return Resp()
102
+
103
+ with unittest.mock.patch.object(publisher_rest.urllib.request,
104
+ "urlopen", fake):
105
+ rows = publisher_rest.query("http://x", "e", "p", "m.malloy",
106
+ "run: x -> { ... }")
107
+ return seen["payload"], rows
108
+
109
+ def test_the_request_asks_for_compact_rows(self):
110
+ payload, _ = self.send({"result": "[]"})
111
+ self.assertIs(payload["compactJson"], True)
112
+ self.assertEqual(payload["query"], "run: x -> { ... }")
113
+
114
+ def test_a_nest_keeps_its_field_names(self):
115
+ # What the server returns under compactJson: `result` is a JSON
116
+ # STRING of plain rows, nested names intact.
117
+ body = {"result": json.dumps(
118
+ [{"carrier": "WN", "n": 2,
119
+ "top_dest": [{"destination_code": "PHX", "d": 1}]}])}
120
+ _, rows = self.send(body)
121
+ self.assertEqual(rows[0]["top_dest"], [{"destination_code": "PHX",
122
+ "d": 1}])
123
+ self.assertNotIn("c0", rows[0]["top_dest"][0])
124
+
125
+ def test_the_envelope_path_still_works(self):
126
+ # Kept as a fallback: a server that ignores the flag still parses.
127
+ body = envelope(["n"], [record({"number_value": 7})])
128
+ _, rows = self.send(body)
129
+ self.assertEqual(rows, [{"n": 7}])
130
+
131
+
76
132
  if __name__ == "__main__":
77
133
  os.chdir(os.path.dirname(__file__))
78
134
  unittest.main()