@malloy-publisher/skills 0.1.49 → 0.1.51

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 (58) hide show
  1. package/package.json +1 -1
  2. package/skills/eval-answer/SKILL.md +4 -3
  3. package/skills/eval-answer/scripts/check_findable.py +16 -6
  4. package/skills/eval-answer/scripts/check_findable_test.py +38 -2
  5. package/skills/eval-answer/scripts/publisher_rest.py +31 -5
  6. package/skills/eval-answer/scripts/publisher_rest_test.py +45 -0
  7. package/skills/eval-diagnose/SKILL.md +1 -1
  8. package/skills/eval-import/SKILL.md +1 -1
  9. package/skills/eval-improve/SKILL.md +1 -1
  10. package/skills/eval-improve/scripts/improve.py +28 -3
  11. package/skills/eval-improve/scripts/improve_test.py +63 -0
  12. package/skills/eval-judge/SKILL.md +1 -1
  13. package/skills/eval-loop/SKILL.md +1 -1
  14. package/skills/eval-loop/scripts/serve.py +29 -7
  15. package/skills/eval-loop/scripts/serve_test.py +15 -1
  16. package/skills/eval-report/SKILL.md +1 -1
  17. package/skills/malloy-analysis/SKILL.md +1 -1
  18. package/skills/malloy-analysis-report/SKILL.md +1 -1
  19. package/skills/malloy-charts/SKILL.md +1 -1
  20. package/skills/malloy-classify/SKILL.md +1 -1
  21. package/skills/malloy-compliance-review/SKILL.md +37 -14
  22. package/skills/malloy-compliance-review/reference/checks.md +17 -8
  23. package/skills/malloy-compliance-review/reference/register.md +5 -4
  24. package/skills/malloy-compliance-review/reference/report.md +13 -2
  25. package/skills/malloy-compliance-review/scripts/compliance.py +207 -40
  26. package/skills/malloy-compliance-review/scripts/compliance_test.py +367 -9
  27. package/skills/malloy-dashboards/SKILL.md +1 -1
  28. package/skills/malloy-define/SKILL.md +1 -1
  29. package/skills/malloy-define-agent/SKILL.md +1 -1
  30. package/skills/malloy-discover/SKILL.md +1 -1
  31. package/skills/malloy-document/SKILL.md +8 -8
  32. package/skills/malloy-getting-started/SKILL.md +2 -2
  33. package/skills/malloy-gotchas-modeling/SKILL.md +2 -2
  34. package/skills/malloy-html-data-apps/SKILL.md +2 -2
  35. package/skills/malloy-lookml-review/SKILL.md +1 -1
  36. package/skills/malloy-lookml-review/reference/_concepts.md +1 -1
  37. package/skills/malloy-lookml-review/reference/curate-visibility.md +3 -3
  38. package/skills/malloy-materialization/SKILL.md +3 -3
  39. package/skills/malloy-materialization/reference/storage.md +1 -1
  40. package/skills/malloy-materialization/reference/tuning.md +1 -1
  41. package/skills/malloy-model/SKILL.md +3 -3
  42. package/skills/malloy-model/reference/access-modifiers.md +4 -2
  43. package/skills/malloy-model-as-you-go/SKILL.md +1 -1
  44. package/skills/malloy-modeling/SKILL.md +2 -2
  45. package/skills/malloy-notebooks/SKILL.md +1 -1
  46. package/skills/malloy-phrase-detection/SKILL.md +1 -1
  47. package/skills/malloy-powerbi-review/SKILL.md +1 -1
  48. package/skills/malloy-powerbi-review/reference/_concepts.md +1 -1
  49. package/skills/malloy-powerbi-review/reference/rls-roles.md +3 -3
  50. package/skills/malloy-publish/SKILL.md +1 -1
  51. package/skills/malloy-queries/SKILL.md +1 -1
  52. package/skills/malloy-reports/SKILL.md +1 -1
  53. package/skills/malloy-review/SKILL.md +1 -1
  54. package/skills/malloy-review/reference/rubric-governance.md +15 -3
  55. package/skills/malloy-review/reference/rubric-rendering.md +2 -2
  56. package/skills/malloy-review/reference/severity-taxonomy.md +1 -0
  57. package/skills/malloy-run-agent/SKILL.md +1 -1
  58. package/skills/malloy-source-unreachable/SKILL.md +1 -1
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@malloy-publisher/skills",
3
3
  "description": "Agent skills for Malloy and the Malloy Publisher",
4
- "version": "0.1.49",
4
+ "version": "0.1.51",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
@@ -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.49
4
+ version: 0.1.51
5
5
  ---
6
6
 
7
7
  # Evaluate One Answer
@@ -19,8 +19,9 @@ revision.
19
19
 
20
20
  Take the question from the stored case (`evals/<set>/cases.jsonl`), never from
21
21
  memory or a truncated console line. Record `question_sha` of the exact text
22
- the answerer saw. Record `servedRevision` from `get_context` or reload, not
23
- the package name: a same-named decoy has been measured for hours.
22
+ the answerer saw. Record `servedRevision` from the package's serving state,
23
+ `GET …/packages/{pkg}/status`, or the `reload_package` result, not the package name: a
24
+ same-named decoy has been measured for hours.
24
25
 
25
26
  ## Step 1: Contamination check, before any score
26
27
 
@@ -209,15 +209,25 @@ def embedding_index(rest_url: str, environment: str, package: str,
209
209
  server-side state as a broken answer key. None also covers a server too old
210
210
  to send the field.
211
211
  """
212
- url = (f"{rest_url.rstrip('/')}/api/v0/environments/"
213
- f"{urllib.parse.quote(environment)}/packages/"
214
- f"{urllib.parse.quote(package)}")
212
+ package_url = (f"{rest_url.rstrip('/')}/api/v0/environments/"
213
+ f"{urllib.parse.quote(environment)}/packages/"
214
+ f"{urllib.parse.quote(package)}")
215
+ # Its own route; a server from before the route carries it on the
216
+ # package resource instead.
215
217
  try:
216
- with urllib.request.urlopen(url, timeout=timeout) as r:
217
- body = json.loads(r.read())
218
+ with urllib.request.urlopen(f"{package_url}/embedding-index",
219
+ timeout=timeout) as r:
220
+ index = json.loads(r.read())
221
+ except urllib.error.HTTPError as e:
222
+ if e.code != 404:
223
+ return None
224
+ try:
225
+ with urllib.request.urlopen(package_url, timeout=timeout) as r:
226
+ index = json.loads(r.read()).get("embeddingIndex")
227
+ except (urllib.error.URLError, OSError, ValueError):
228
+ return None
218
229
  except (urllib.error.URLError, OSError, ValueError):
219
230
  return None
220
- index = body.get("embeddingIndex")
221
231
  if not isinstance(index, dict) or not isinstance(index.get("status"), str):
222
232
  return None
223
233
  return index
@@ -531,17 +531,53 @@ class IndexWait(unittest.TestCase):
531
531
 
532
532
  class EmbeddingIndexRead(unittest.TestCase):
533
533
  def read(self, payload):
534
+ """Read the index from a server holding `payload` as its package
535
+ resource. A payload carrying `embeddingIndex` is a current server,
536
+ which serves the object from `GET …/embedding-index`; one without it
537
+ is an older server, whose `/embedding-index` 404s."""
534
538
  import json as _json
535
539
 
536
540
  class Resp:
541
+ def __init__(self, body): self.body = body
537
542
  def __enter__(self): return self
538
543
  def __exit__(self, *a): return False
539
- def read(self): return _json.dumps(payload).encode()
544
+ def read(self): return _json.dumps(self.body).encode()
545
+
546
+ def urlopen(url, timeout=None):
547
+ if url.endswith("/embedding-index"):
548
+ if "embeddingIndex" in payload:
549
+ return Resp(payload["embeddingIndex"])
550
+ raise check_findable.urllib.error.HTTPError(
551
+ url, 404, "Not Found", None, None)
552
+ return Resp(payload)
540
553
 
541
554
  with mock.patch.object(check_findable.urllib.request, "urlopen",
542
- lambda *a, **k: Resp()):
555
+ urlopen):
543
556
  return check_findable.embedding_index("http://x", "e", "p")
544
557
 
558
+ def test_an_older_server_is_read_from_the_package_resource(self):
559
+ idx = {"status": "ready"}
560
+ import json as _json
561
+ calls = []
562
+
563
+ class Resp:
564
+ def __enter__(self): return self
565
+ def __exit__(self, *a): return False
566
+ def read(self): return _json.dumps({"embeddingIndex": idx}).encode()
567
+
568
+ def urlopen(url, timeout=None):
569
+ calls.append(url)
570
+ if url.endswith("/embedding-index"):
571
+ raise check_findable.urllib.error.HTTPError(
572
+ url, 404, "Not Found", None, None)
573
+ return Resp()
574
+
575
+ with mock.patch.object(check_findable.urllib.request, "urlopen",
576
+ urlopen):
577
+ self.assertEqual(
578
+ check_findable.embedding_index("http://x", "e", "p"), idx)
579
+ self.assertEqual(len(calls), 2)
580
+
545
581
  def test_the_whole_object_comes_back_so_reason_is_not_lost(self):
546
582
  idx = {"status": "error", "reason": "cooldown",
547
583
  "lastError": {"message": "m"}}
@@ -205,7 +205,8 @@ def served_model_path(base: str, environment: str, package: str,
205
205
 
206
206
 
207
207
  def package_identity(base: str, environment: str, package: str) -> dict[str, str]:
208
- """What the server says it is serving: `sourceContentSha`, `servedRevision`.
208
+ """What the server says it is serving: `sourceContentSha`, `servedRevision`,
209
+ read from the package's serving state (`GET …/packages/{pkg}/status`).
209
210
 
210
211
  Take these rather than computing your own. `sourceContentSha` is a content
211
212
  hash over EVERY model path in the package, so it moves when an imported file
@@ -218,10 +219,35 @@ def package_identity(base: str, environment: str, package: str) -> dict[str, str
218
219
  directory. So `publisher.json` version pins nothing (nothing reads it) and a
219
220
  set of goldens living inside the package needs its own hash.
220
221
  """
222
+ keys = ("sourceContentSha", "servedRevision")
223
+ pkg_path = (f"api/v0/environments/{urllib.parse.quote(environment)}"
224
+ f"/packages/{urllib.parse.quote(package)}")
225
+ # The package's serving state; for a versioned package, its `latest`'s.
221
226
  try:
222
- d = get_json(base, f"api/v0/environments/{urllib.parse.quote(environment)}"
223
- f"/packages/{urllib.parse.quote(package)}")
227
+ d = get_json(base, f"{pkg_path}/status")
228
+ if any(isinstance(d.get(k), str) for k in keys):
229
+ return {k: d[k] for k in keys if isinstance(d.get(k), str)}
230
+ except Exception: # noqa: BLE001
231
+ pass
232
+ # A server from before that route: the package's `/status` entry carries
233
+ # them, one entry per version held, and the one serving `latest` answers.
234
+ try:
235
+ status = get_json(base, "api/v0/status")
236
+ for env in status.get("environments") or []:
237
+ if env.get("name") != environment:
238
+ continue
239
+ entries = [p for p in env.get("packages") or []
240
+ if p.get("name") == package]
241
+ entry = next((p for p in entries
242
+ if p.get("versionId") in (None, p.get("latestVersion"))),
243
+ entries[0] if entries else None)
244
+ if entry and any(isinstance(entry.get(k), str) for k in keys):
245
+ return {k: entry[k] for k in keys if isinstance(entry.get(k), str)}
246
+ except Exception: # noqa: BLE001
247
+ pass
248
+ # Older still: on the package resource.
249
+ try:
250
+ d = get_json(base, pkg_path)
224
251
  except Exception: # noqa: BLE001
225
252
  return {}
226
- return {k: d[k] for k in ("sourceContentSha", "servedRevision")
227
- if isinstance(d.get(k), str)}
253
+ return {k: d[k] for k in keys if isinstance(d.get(k), str)}
@@ -149,6 +149,51 @@ class CompactJson(unittest.TestCase):
149
149
  self.assertEqual(rows, [{"n": 7}])
150
150
 
151
151
 
152
+
153
+ class PackageIdentityTests(unittest.TestCase):
154
+ """The served identity comes from the package's serving state, and on older
155
+ servers from its `/status` entry or the package resource."""
156
+
157
+ def identity(self, responses):
158
+ def get_json(base, path):
159
+ if path in responses:
160
+ return responses[path]
161
+ raise OSError("HTTP Error 404")
162
+ with unittest.mock.patch.object(publisher_rest, "get_json", get_json):
163
+ return publisher_rest.package_identity("http://x", "env", "pkg")
164
+
165
+ def test_the_serving_state_answers(self):
166
+ state = {"name": "pkg", "sourceContentSha": "abc", "servedRevision": "r1"}
167
+ self.assertEqual(
168
+ self.identity({"api/v0/environments/env/packages/pkg/status": state}),
169
+ {"sourceContentSha": "abc", "servedRevision": "r1"})
170
+
171
+ def test_a_server_without_the_route_answers_from_its_status_entry(self):
172
+ status = {"environments": [{"name": "env", "packages": [
173
+ {"name": "other", "sourceContentSha": "zzz"},
174
+ {"name": "pkg", "sourceContentSha": "abc", "servedRevision": "r1"},
175
+ ]}]}
176
+ self.assertEqual(self.identity({"api/v0/status": status}),
177
+ {"sourceContentSha": "abc", "servedRevision": "r1"})
178
+
179
+ def test_a_versioned_package_answers_from_its_latest_version(self):
180
+ status = {"environments": [{"name": "env", "packages": [
181
+ {"name": "pkg", "versionId": "1.0.0", "latestVersion": "1.1.0",
182
+ "sourceContentSha": "old"},
183
+ {"name": "pkg", "versionId": "1.1.0", "latestVersion": "1.1.0",
184
+ "sourceContentSha": "new"},
185
+ ]}]}
186
+ self.assertEqual(self.identity({"api/v0/status": status}),
187
+ {"sourceContentSha": "new"})
188
+
189
+ def test_an_older_server_answers_from_the_package_resource(self):
190
+ status = {"environments": [{"name": "env", "packages": [{"name": "pkg"}]}]}
191
+ pkg = {"sourceContentSha": "abc", "servedRevision": "r1"}
192
+ self.assertEqual(
193
+ self.identity({"api/v0/status": status,
194
+ "api/v0/environments/env/packages/pkg": pkg}),
195
+ {"sourceContentSha": "abc", "servedRevision": "r1"})
196
+
152
197
  if __name__ == "__main__":
153
198
  os.chdir(os.path.dirname(__file__))
154
199
  unittest.main()
@@ -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.49
4
+ version: 0.1.51
5
5
  ---
6
6
 
7
7
  # Diagnose One Answer
@@ -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.49
4
+ version: 0.1.51
5
5
  ---
6
6
 
7
7
  # Import questions into an eval set
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: eval-improve
3
3
  description: 'Make the smallest safe edit that closes a diagnosed gap the model repo owns: a Malloy model edit, or a package skill under skills/. Every factual claim carries a probe receipt. 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.49
4
+ version: 0.1.51
5
5
  ---
6
6
 
7
7
  # Improve the Model
@@ -77,9 +77,34 @@ IMPROVE_TOOLS = ("mcp__publisher__get_context",
77
77
  "Read", "Edit", "Write", "Grep", "Glob",
78
78
  "Bash(bash ./sync_and_reload.sh)")
79
79
 
80
- # REST, not MCP: an MCP reply escapes its inner JSON, so grepping it for the sha matches nothing.
81
- RELOAD = """curl -s -m 60 "{rest_url}/api/v0/environments/{environment}/packages/{package}?reload=true" \\
82
- | grep -o '"sourceContentSha":"[^"]*"' | head -1"""
80
+ # REST, not MCP: an MCP reply escapes its inner JSON. The sha is read from the
81
+ # package's serving state after the reload (the reload response itself carries
82
+ # it only while the server inlines the serving fields). A failed reload prints
83
+ # its error and stops, so the previous load's sha is never shown for it.
84
+ RELOAD = """reload_body=$(mktemp)
85
+ reload_code=$(curl -s -m 60 -o "$reload_body" -w "%{{http_code}}" "{rest_url}/api/v0/environments/{environment}/packages/{package}?reload=true" || echo 000)
86
+ if [ "$reload_code" != "200" ]; then
87
+ echo "reload failed (HTTP $reload_code):"; cat "$reload_body"; echo; rm -f "$reload_body"; exit 1
88
+ fi
89
+ rm -f "$reload_body"
90
+ python3 -c '
91
+ import json, urllib.request
92
+ base = "{rest_url}/api/v0"
93
+ def get(path):
94
+ try:
95
+ with urllib.request.urlopen(base + path, timeout=30) as r:
96
+ return json.load(r)
97
+ except Exception:
98
+ return {{}}
99
+ sha = get("/environments/{environment}/packages/{package}/status").get("sourceContentSha")
100
+ if not sha: # a server from before that route: the package`s /status entry
101
+ for e in get("/status").get("environments") or []:
102
+ for p in (e.get("packages") or []) if e.get("name") == "{environment}" else []:
103
+ if p.get("name") == "{package}" and p.get("sourceContentSha"):
104
+ sha = p["sourceContentSha"]
105
+ if sha:
106
+ print("\\"sourceContentSha\\":\\"%s\\"" % sha)
107
+ '"""
83
108
 
84
109
  SYNC_SCRIPT = """#!/bin/bash
85
110
  set -euo pipefail
@@ -282,5 +282,68 @@ class Grants(unittest.TestCase):
282
282
  self.assertIn(f"mcp__publisher__{t}", improve.IMPROVE_TOOLS)
283
283
 
284
284
 
285
+ class ReloadReceipt(unittest.TestCase):
286
+ """The reload helper prints the served sha: from the package's serving
287
+ state, or from its `/status` entry on a server from before that route.
288
+ A reload that fails prints its error instead, never the old sha."""
289
+
290
+ PKG = "/api/v0/environments/env/packages/pkg"
291
+
292
+ def run_reload(self, routes):
293
+ import http.server
294
+ import json
295
+ import threading
296
+
297
+ class Handler(http.server.BaseHTTPRequestHandler):
298
+ def do_GET(self):
299
+ path = self.path.split("?")[0]
300
+ body = routes.get(path)
301
+ code, body = body if isinstance(body, tuple) else (
302
+ 200 if body is not None else 404, body)
303
+ self.send_response(code)
304
+ self.end_headers()
305
+ self.wfile.write(json.dumps(body or {}).encode())
306
+
307
+ def log_message(self, *args):
308
+ pass
309
+
310
+ server = http.server.HTTPServer(("127.0.0.1", 0), Handler)
311
+ threading.Thread(target=server.serve_forever, daemon=True).start()
312
+ try:
313
+ cmd = improve.RELOAD.format(
314
+ rest_url=f"http://127.0.0.1:{server.server_port}",
315
+ environment="env", package="pkg")
316
+ return subprocess.run(["bash", "-c", cmd], capture_output=True,
317
+ text=True, timeout=60).stdout.strip()
318
+ finally:
319
+ server.shutdown()
320
+
321
+ def test_reads_the_serving_state(self):
322
+ out = self.run_reload({
323
+ self.PKG: {"name": "pkg"},
324
+ "/api/v0/environments/env/packages/pkg/status":
325
+ {"sourceContentSha": "abc"},
326
+ })
327
+ self.assertEqual(out, '"sourceContentSha":"abc"')
328
+
329
+ def test_falls_back_to_the_status_entry_on_an_older_server(self):
330
+ out = self.run_reload({
331
+ self.PKG: {"name": "pkg"},
332
+ "/api/v0/status": {"environments": [{"name": "env", "packages": [
333
+ {"name": "pkg", "sourceContentSha": "old"}]}]},
334
+ })
335
+ self.assertEqual(out, '"sourceContentSha":"old"')
336
+
337
+ def test_a_failed_reload_prints_its_error_and_no_sha(self):
338
+ out = self.run_reload({
339
+ self.PKG: (400, {"message": "bad.malloy: refused"}),
340
+ "/api/v0/environments/env/packages/pkg/status":
341
+ {"sourceContentSha": "stale"},
342
+ })
343
+ self.assertIn("reload failed (HTTP 400)", out)
344
+ self.assertIn("bad.malloy: refused", out)
345
+ self.assertNotIn("stale", out)
346
+
347
+
285
348
  if __name__ == "__main__":
286
349
  unittest.main()
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: eval-judge
3
3
  description: 'Decide whether ONE answer matches its golden, and say whether you believe the golden. Read this before emitting any verdict. Covers containment, column pairing, near_match, refusals, and the gold_status judgement. Use when scoring an attempt in an evaluation run; never to conduct a run (eval-loop), diagnose a failure (eval-diagnose) or edit a model (eval-improve).'
4
- version: 0.1.49
4
+ version: 0.1.51
5
5
  ---
6
6
 
7
7
  # The judge
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: eval-loop
3
3
  description: 'Conduct a local Publisher evaluation loop in five steps: scrape/run, eval, diagnose, improve, checkpoint. You are the conductor: import cases into the file ledger, spawn a blind answerer, then run eval-answer, eval-diagnose, and eval-improve. Persistence is plain files: the set in the model package''s evals/ directory, runs in the set''s workdir; checkpoints are git commits of the model repo. `eval.py` runs each step from the set''s eval.toml. Use to score a model, diagnose failures, improve behind an acceptance check, or roll back a bad direction.'
4
- version: 0.1.49
4
+ version: 0.1.51
5
5
  ---
6
6
 
7
7
  # The Evaluation Loop
@@ -53,8 +53,9 @@ run that starts the moment the REST root replies would record those empty
53
53
  answers as retrieval misses.
54
54
 
55
55
  It drives one ranking call (older servers only started the sync on the first
56
- one), then polls the package resource until `embeddingIndex.status` reaches a
57
- terminal value and prints it. The values are `lexical` (no provider: a mode),
56
+ one), then polls `GET …/packages/{package}/embedding-index` (the package
57
+ resource's `embeddingIndex` on a server from before that route) until its
58
+ `status` reaches a terminal value and prints it. The values are `lexical` (no provider: a mode),
58
59
  `indexing` (the only non-terminal one), `ready`, and `error`, which carries a
59
60
  `reason` and `lastError.message` that are printed. Only `ready` licenses
60
61
  reading a run's discoverability findings. A package resource with no
@@ -95,6 +96,7 @@ import socket
95
96
  import subprocess
96
97
  import sys
97
98
  import time
99
+ import urllib.error
98
100
  import urllib.request
99
101
 
100
102
  sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent.parent
@@ -203,6 +205,27 @@ def warm_arguments(environment: str, package: str) -> dict:
203
205
  }
204
206
 
205
207
 
208
+ def read_index(port: int, environment: str, package: str) -> dict | None:
209
+ """The package's embedding-index object, or None when the server has none.
210
+
211
+ Read from `GET …/embedding-index`; a server from before that route carries
212
+ the object on the package resource as `embeddingIndex`, so a 404 there
213
+ falls back to it. A transport failure raises, for the caller's retry.
214
+ """
215
+ package_url = (f"http://localhost:{port}/api/v0/environments/"
216
+ f"{environment}/packages/{package}")
217
+ try:
218
+ with urllib.request.urlopen(f"{package_url}/embedding-index",
219
+ timeout=10) as r:
220
+ return json.loads(r.read().decode())
221
+ except urllib.error.HTTPError as e:
222
+ if e.code != 404:
223
+ raise
224
+ with urllib.request.urlopen(package_url, timeout=10) as r:
225
+ index = json.loads(r.read().decode()).get("embeddingIndex")
226
+ return index if isinstance(index, dict) else None
227
+
228
+
206
229
  def index_status(package_payload: dict) -> str | None:
207
230
  """`embeddingIndex.status` from a package resource, or None if absent.
208
231
 
@@ -235,13 +258,12 @@ def warm_retrieval(port: int, mcp_port: int, environment: str, package: str,
235
258
  return None, f"warm-up call failed: {e}"
236
259
 
237
260
  url = (f"http://localhost:{port}/api/v0/environments/{environment}"
238
- f"/packages/{package}")
261
+ f"/packages/{package}/embedding-index")
239
262
  deadline = time.time() + wait
240
263
  status, last_error, read_one, index = None, None, False, {}
241
264
  while time.time() < deadline:
242
265
  try:
243
- with urllib.request.urlopen(url, timeout=10) as r:
244
- payload = json.loads(r.read().decode())
266
+ read = read_index(port, environment, package)
245
267
  except Exception as e: # noqa: BLE001
246
268
  # A transport failure is not a capability verdict. Collapsing it
247
269
  # into `status = None` handed None a second meaning `index_status`
@@ -255,13 +277,13 @@ def warm_retrieval(port: int, mcp_port: int, environment: str, package: str,
255
277
  time.sleep(2)
256
278
  continue
257
279
  read_one = True
258
- status = index_status(payload)
280
+ status = index_status({"embeddingIndex": read})
259
281
  if status is None:
260
282
  return None, ("no embeddingIndex on the package resource; this "
261
283
  "server has no embedding provider or predates the "
262
284
  "status field, so every run against it is LEXICAL "
263
285
  "-- do not report discoverability findings from it")
264
- index = payload["embeddingIndex"]
286
+ index = read
265
287
  if status in TERMINAL_INDEX_STATES:
266
288
  break
267
289
  time.sleep(2)
@@ -163,10 +163,24 @@ class WarmRetrievalSeparatesFailureFromAbsence(unittest.TestCase):
163
163
  def read(self):
164
164
  return json.dumps(self.payload).encode()
165
165
 
166
+ # Each scripted outcome is a package-resource payload or an error. A
167
+ # payload carrying `embeddingIndex` is served the way a current server
168
+ # serves it, from `GET …/embedding-index`; one without it is an older
169
+ # server, whose `/embedding-index` 404s and whose package resource
170
+ # answers instead.
171
+ pending = []
172
+
166
173
  def urlopen(req, timeout=None):
167
- r = next(it)
174
+ url = req if isinstance(req, str) else req.full_url
175
+ r = pending.pop() if pending else next(it)
168
176
  if isinstance(r, Exception):
169
177
  raise r
178
+ if url.endswith("/embedding-index"):
179
+ if "embeddingIndex" in r:
180
+ return Resp(r["embeddingIndex"])
181
+ pending.append(r)
182
+ raise serve.urllib.error.HTTPError(url, 404, "Not Found",
183
+ None, None)
170
184
  return Resp(r)
171
185
 
172
186
  with mock.patch.object(serve, "_mcp_call", lambda *a, **k: None), \
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: eval-report
3
3
  description: 'Write the summary a person reads after an evaluation run: what ran, what it scored, which failures were the MODEL and which were the EVAL itself (out of turns, contaminated, unestablished golden, unreadable judge), what was learned, and what to do next. Every claim links to the artifact behind it. Use at the end of any run, before quoting a number to anyone. Never scores an answer (eval-answer) or decides who owns a failure (eval-diagnose).'
4
- version: 0.1.49
4
+ version: 0.1.51
5
5
  ---
6
6
 
7
7
  # Report a Run
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: malloy-analysis
3
3
  description: "Read before your first get_context call on any business or data question. The required workflow, not background: which source, which rows, how to verify. Skipping it causes confident wrong answers."
4
- version: 0.1.49
4
+ version: 0.1.51
5
5
  ---
6
6
 
7
7
  <!-- Copyright (c) Credible Data Inc. SPDX-License-Identifier: MIT -->
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: malloy-analysis-report
3
3
  description: Combine validated Malloy queries into a report (a linear report, read top to bottom). Use when the user asks to "create a report", "combine these into a report", or wants a persistent multi-query artifact.
4
- version: 0.1.49
4
+ version: 0.1.51
5
5
  ---
6
6
 
7
7
  <!--
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: malloy-charts
3
3
  description: Read before choosing a chart or adding a chart annotation. Chart types, tag syntax and scale rules, KPI cards, dashboards, sparklines, and the renderer mistakes that fail silently.
4
- version: 0.1.49
4
+ version: 0.1.51
5
5
  ---
6
6
  <!--
7
7
  Copyright (c) Credible Data Inc.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: malloy-classify
3
3
  description: 'Propose #(classification) annotations for a Malloy model from an access policy''s class scheme, backed by aggregate value evidence from the served data, and show them as a diff before writing anything. Use when the user asks to "classify the model", "tag PHI/PII", "label sensitive fields", or adds fields to a package that a policy governs, and before running malloy-compliance-review on a model nobody has classified.'
4
- version: 0.1.49
4
+ version: 0.1.51
5
5
  ---
6
6
 
7
7
  <!--