@malloy-publisher/skills 0.1.52 → 0.1.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skills/eval-answer/SKILL.md +1 -1
- package/skills/eval-diagnose/SKILL.md +1 -1
- package/skills/eval-import/SKILL.md +1 -1
- package/skills/eval-improve/SKILL.md +9 -7
- package/skills/eval-improve/scripts/improve.py +62 -8
- package/skills/eval-improve/scripts/improve_test.py +66 -0
- package/skills/eval-judge/SKILL.md +1 -1
- package/skills/eval-loop/SKILL.md +1 -1
- package/skills/eval-loop/scripts/build_run_package.py +27 -2
- package/skills/eval-loop/scripts/build_run_package_test.py +19 -0
- package/skills/eval-loop/scripts/run_baseline.py +13 -3
- package/skills/eval-loop/scripts/serve.py +29 -0
- package/skills/eval-loop/scripts/serve_test.py +40 -0
- package/skills/eval-loop/templates/eval-run-package/README.md +14 -16
- package/skills/eval-report/SKILL.md +1 -1
- package/skills/malloy-analysis/SKILL.md +1 -1
- package/skills/malloy-analysis-report/SKILL.md +1 -1
- package/skills/malloy-charts/SKILL.md +1 -1
- package/skills/malloy-classify/SKILL.md +1 -1
- package/skills/malloy-compliance-review/SKILL.md +1 -1
- package/skills/malloy-dashboards/SKILL.md +1 -1
- package/skills/malloy-define/SKILL.md +1 -1
- package/skills/malloy-define-agent/SKILL.md +1 -1
- package/skills/malloy-discover/SKILL.md +2 -1
- package/skills/malloy-document/SKILL.md +1 -1
- package/skills/malloy-excel-review/NOTICE +13 -0
- package/skills/malloy-excel-review/SKILL.md +168 -0
- package/skills/malloy-excel-review/fixtures/README.md +79 -0
- package/skills/malloy-excel-review/fixtures/build_fixture.py +882 -0
- package/skills/malloy-excel-review/fixtures/fixture.xlsx +0 -0
- package/skills/malloy-excel-review/fixtures/fixture_1904.xlsx +0 -0
- package/skills/malloy-excel-review/reference/_concepts.md +186 -0
- package/skills/malloy-excel-review/reference/cookbook-lookup-aggregate.md +1208 -0
- package/skills/malloy-excel-review/reference/cookbook-pivot.md +804 -0
- package/skills/malloy-excel-review/reference/cookbook-scenario.md +934 -0
- package/skills/malloy-excel-review/reference/discover.md +334 -0
- package/skills/malloy-excel-review/reference/limitations.md +175 -0
- package/skills/malloy-excel-review/reference/parity.md +304 -0
- package/skills/malloy-excel-review/reference/power-pivot.md +129 -0
- package/skills/malloy-excel-review/reference/recover-sources.md +332 -0
- package/skills/malloy-excel-review/reference/review-coverage.md +145 -0
- package/skills/malloy-excel-review/reference/translate-formulas.md +391 -0
- package/skills/malloy-excel-review/scripts/classify_workbook.py +7443 -0
- package/skills/malloy-excel-review/scripts/classify_workbook_connections_test.py +1695 -0
- package/skills/malloy-excel-review/scripts/classify_workbook_test.py +5251 -0
- package/skills/malloy-excel-review/scripts/fixture_test.py +502 -0
- package/skills/malloy-getting-started/SKILL.md +3 -3
- package/skills/malloy-gotchas-modeling/SKILL.md +1 -1
- package/skills/malloy-html-data-apps/SKILL.md +3 -3
- package/skills/malloy-html-data-apps/reference/runtime.md +13 -13
- package/skills/malloy-lookml-review/SKILL.md +1 -1
- package/skills/malloy-materialization/SKILL.md +44 -5
- package/skills/malloy-materialization/reference/annotation.md +9 -5
- package/skills/malloy-materialization/reference/refresh.md +1 -1
- package/skills/malloy-materialization/reference/storage.md +4 -4
- package/skills/malloy-materialization/reference/tuning.md +1 -1
- package/skills/malloy-model/SKILL.md +1 -1
- package/skills/malloy-model-as-you-go/SKILL.md +2 -2
- package/skills/malloy-modeling/SKILL.md +3 -1
- package/skills/malloy-notebooks/SKILL.md +1 -1
- package/skills/malloy-phrase-detection/SKILL.md +1 -1
- package/skills/malloy-powerbi-review/SKILL.md +1 -1
- package/skills/malloy-publish/SKILL.md +3 -3
- package/skills/malloy-queries/SKILL.md +1 -1
- package/skills/malloy-reports/SKILL.md +3 -3
- package/skills/malloy-review/SKILL.md +1 -1
- package/skills/malloy-run-agent/SKILL.md +1 -1
- package/skills/malloy-source-unreachable/SKILL.md +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: eval-answer
|
|
3
3
|
description: 'Score one analytical answer against a verified golden, and score which of the entities the golden depends on retrieval delivered to the answerer. Run the contamination checklist, re-execute the submitted query yourself, then spawn a judge subagent per skill:eval-judge. Append attempt, tool_call, score, and retrieval_score events to the file ledger (reference/ledger-schema.md). Never explain the failure (eval-diagnose) or edit the model (eval-improve). Use when asked whether an answer was correct, to score a run, or to baseline a model.'
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.53
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Evaluate One Answer
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: eval-diagnose
|
|
3
3
|
description: 'Diagnose why a scored answer failed and who owns the fix, then cluster the failures by shared root cause. Walk dataset, agent-call, get_context/model, get_context/retrieval, construction, then model-definition. Append issue events to the file ledger, linked by traceId, one per cluster. Use after eval-answer, when triaging a run, or before changing a model. Does not edit the model (eval-improve).'
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.53
|
|
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.
|
|
4
|
+
version: 0.1.53
|
|
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.
|
|
4
|
+
version: 0.1.53
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Improve the Model
|
|
@@ -117,12 +117,14 @@ publishing.
|
|
|
117
117
|
|
|
118
118
|
Know which copy of the file the server actually reads. Hosts commonly serve a
|
|
119
119
|
copy of the package rather than your working tree, so editing the model repo
|
|
120
|
-
and reloading
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
120
|
+
and reloading changes nothing: the reload succeeds, and a verification probe
|
|
121
|
+
quietly tests the old model. Confirm the edit reached what is served before
|
|
122
|
+
you trust a probe, and keep the model repo the source of truth that gets
|
|
123
|
+
committed. On open-source Publisher, a package outside a watch-mounted
|
|
124
|
+
environment is a published version, which is immutable: a reload answers it
|
|
125
|
+
`mode: unchanged` and never reads your files. Serve the model in watch mode
|
|
126
|
+
(`eval.py serve model --set <set> --watch-env <env> --reinit`, then
|
|
127
|
+
`improve.py --watch-mode`). Other hosts distinguish a draft from a published version.
|
|
126
128
|
|
|
127
129
|
## Step 3: One smallest edit
|
|
128
130
|
|
|
@@ -39,13 +39,18 @@ events describe diffs that never existed alone.
|
|
|
39
39
|
|
|
40
40
|
MAKING AN EDIT VISIBLE TO THE SERVER
|
|
41
41
|
|
|
42
|
-
Publisher
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
model. `--watch-mode` says Publisher was started with
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
42
|
+
Publisher serves a package outside a watched environment as a published
|
|
43
|
+
version under `publisher_data/<env>/<pkg>/<version>/`, which is immutable:
|
|
44
|
+
editing the repo and reloading changes nothing, and the verification probe
|
|
45
|
+
quietly tests the old model. `--watch-mode` says Publisher was started with
|
|
46
|
+
`--watch-env` (`eval.py serve model --set <set> --watch-env <env>`), which symlinks
|
|
47
|
+
local-dir packages in place and makes edits live; then the helper is just a
|
|
48
|
+
reload. Without it the helper syncs the edit onto the served copy first, which
|
|
49
|
+
works only on a server with versioning off. Either way a package the server
|
|
50
|
+
serves as a published version is refused before any agent starts (the server
|
|
51
|
+
is asked, in both modes): nothing synced there is served, the sync would
|
|
52
|
+
delete it, and a reload of it answers 200 without reading the edit. Either way
|
|
53
|
+
the agent gets one generated script rather than a general shell.
|
|
49
54
|
"""
|
|
50
55
|
from __future__ import annotations
|
|
51
56
|
|
|
@@ -55,6 +60,7 @@ import pathlib
|
|
|
55
60
|
import subprocess
|
|
56
61
|
import sys
|
|
57
62
|
import time
|
|
63
|
+
import urllib.request
|
|
58
64
|
from typing import Any
|
|
59
65
|
|
|
60
66
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent.parent
|
|
@@ -116,6 +122,37 @@ echo "reloaded; if sourceContentSha did not change, the edit did not reach the s
|
|
|
116
122
|
SYNC_HELPER = "sync_and_reload.sh"
|
|
117
123
|
|
|
118
124
|
|
|
125
|
+
def served_version(rest_url: str | None, environment: str,
|
|
126
|
+
package: str) -> str | None:
|
|
127
|
+
"""The version the model server serves this package as, or None.
|
|
128
|
+
|
|
129
|
+
The server's own answer: a published version's resource carries its
|
|
130
|
+
`versionId`, and a watch-mounted or copied package has none. A version
|
|
131
|
+
is immutable, so neither the sync nor watch mode reaches it. None also
|
|
132
|
+
when the server cannot be asked; the folder check below still stands.
|
|
133
|
+
"""
|
|
134
|
+
if not rest_url:
|
|
135
|
+
return None
|
|
136
|
+
url = (f"{rest_url.rstrip('/')}/api/v0/environments/{environment}"
|
|
137
|
+
f"/packages/{package}")
|
|
138
|
+
try:
|
|
139
|
+
with urllib.request.urlopen(url, timeout=30) as r:
|
|
140
|
+
return json.load(r).get("versionId") or None
|
|
141
|
+
except Exception:
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def served_as_versions(served: pathlib.Path) -> bool:
|
|
146
|
+
"""Whether Publisher serves this package folder as published versions.
|
|
147
|
+
|
|
148
|
+
A version lives in `<pkg>/<version>/`, so the folder holds no
|
|
149
|
+
publisher.json of its own. A copy (versioning off) does, and a watch
|
|
150
|
+
mount is a symlink to the source.
|
|
151
|
+
"""
|
|
152
|
+
return (served.is_dir() and not served.is_symlink()
|
|
153
|
+
and not (served / "publisher.json").exists())
|
|
154
|
+
|
|
155
|
+
|
|
119
156
|
def git(args: list[str], cwd: pathlib.Path) -> str:
|
|
120
157
|
return subprocess.run(["git", *args], cwd=cwd, capture_output=True,
|
|
121
158
|
text=True).stdout
|
|
@@ -386,7 +423,9 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
386
423
|
ap.add_argument("--server-root", type=pathlib.Path, default=None,
|
|
387
424
|
help="Publisher SERVER_ROOT; only needed without watch "
|
|
388
425
|
"mode. Each accepted edit is rsynced onto the copy "
|
|
389
|
-
"this root serves and reloaded
|
|
426
|
+
"this root serves and reloaded, which works only "
|
|
427
|
+
"with versioning off: a root serving published "
|
|
428
|
+
"versions is refused. Default: the one "
|
|
390
429
|
"`serve.py --role model` uses, when it exists")
|
|
391
430
|
ap.add_argument("--watch-mode", action="store_true",
|
|
392
431
|
help="Publisher was started with --watch-env, so edits are "
|
|
@@ -483,6 +522,21 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
483
522
|
print("warning: no --watch-mode and no --server-root, so the agent "
|
|
484
523
|
"cannot sync its edit into Publisher's snapshot; every probe it "
|
|
485
524
|
"runs will test the unedited model")
|
|
525
|
+
# Asked in both modes: --watch-mode is the caller's claim, and a server
|
|
526
|
+
# started with --watch-env over a preserved root still serves a version.
|
|
527
|
+
version = served_version(a.rest_url, a.environment, a.package)
|
|
528
|
+
served = (a.server_root / "publisher_data" / a.environment / a.package
|
|
529
|
+
if a.server_root and not a.watch_mode else None)
|
|
530
|
+
if version or (served is not None and served_as_versions(served)):
|
|
531
|
+
what = (f"the model server serves {a.package} as published version "
|
|
532
|
+
f"{version}" if version else f"{served} holds published versions")
|
|
533
|
+
why = ("so watch mode is not in effect for it" if a.watch_mode else
|
|
534
|
+
"no edit synced there is ever served, and the sync would "
|
|
535
|
+
"delete them")
|
|
536
|
+
raise SystemExit(
|
|
537
|
+
f"{what}, which is immutable: {why}. Fix: restart the model "
|
|
538
|
+
f"server in watch mode, `eval.py serve model --set {a.set_dir} "
|
|
539
|
+
f"--watch-env {a.environment} --reinit`, then pass --watch-mode")
|
|
486
540
|
|
|
487
541
|
events = read_jsonl(a.run / "events.jsonl")
|
|
488
542
|
cases = {c["qid"]: c for c in read_jsonl(a.set_dir / "cases.jsonl")}
|
|
@@ -229,6 +229,72 @@ class TruthEnvironment(unittest.TestCase):
|
|
|
229
229
|
|
|
230
230
|
|
|
231
231
|
|
|
232
|
+
class ServedAsVersions(unittest.TestCase):
|
|
233
|
+
"""The snapshot sync (`rsync --delete` onto the served folder) is safe only
|
|
234
|
+
over a copy. Over published versions it serves nothing and deletes them,
|
|
235
|
+
so it is refused."""
|
|
236
|
+
|
|
237
|
+
def setUp(self):
|
|
238
|
+
self.root = pathlib.Path(tempfile.mkdtemp())
|
|
239
|
+
self.addCleanup(shutil.rmtree, self.root, True)
|
|
240
|
+
|
|
241
|
+
def test_a_folder_of_versions_is_versions(self):
|
|
242
|
+
served = self.root / "pkg"
|
|
243
|
+
(served / "1.0.0").mkdir(parents=True)
|
|
244
|
+
(served / "1.0.0" / "publisher.json").write_text("{}")
|
|
245
|
+
self.assertTrue(improve.served_as_versions(served))
|
|
246
|
+
|
|
247
|
+
def test_a_copy_is_not(self):
|
|
248
|
+
served = self.root / "pkg"
|
|
249
|
+
served.mkdir()
|
|
250
|
+
(served / "publisher.json").write_text("{}")
|
|
251
|
+
self.assertFalse(improve.served_as_versions(served))
|
|
252
|
+
|
|
253
|
+
def test_a_watch_mount_is_not(self):
|
|
254
|
+
source = self.root / "src"
|
|
255
|
+
source.mkdir()
|
|
256
|
+
served = self.root / "pkg"
|
|
257
|
+
served.symlink_to(source, target_is_directory=True)
|
|
258
|
+
self.assertFalse(improve.served_as_versions(served))
|
|
259
|
+
|
|
260
|
+
def test_a_missing_folder_is_not(self):
|
|
261
|
+
self.assertFalse(improve.served_as_versions(self.root / "nothing"))
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class ServedVersion(unittest.TestCase):
|
|
265
|
+
"""The server says whether it serves the package as a published version."""
|
|
266
|
+
|
|
267
|
+
def serve(self, body: bytes):
|
|
268
|
+
import http.server
|
|
269
|
+
import threading
|
|
270
|
+
|
|
271
|
+
class H(http.server.BaseHTTPRequestHandler):
|
|
272
|
+
def do_GET(self):
|
|
273
|
+
self.send_response(200)
|
|
274
|
+
self.end_headers()
|
|
275
|
+
self.wfile.write(body)
|
|
276
|
+
|
|
277
|
+
def log_message(self, *a):
|
|
278
|
+
pass
|
|
279
|
+
|
|
280
|
+
srv = http.server.HTTPServer(("127.0.0.1", 0), H)
|
|
281
|
+
threading.Thread(target=srv.serve_forever, daemon=True).start()
|
|
282
|
+
self.addCleanup(srv.shutdown)
|
|
283
|
+
return f"http://127.0.0.1:{srv.server_port}"
|
|
284
|
+
|
|
285
|
+
def test_a_version_is_named(self):
|
|
286
|
+
base = self.serve(b'{"name": "ecommerce", "versionId": "1.0.0"}')
|
|
287
|
+
self.assertEqual(improve.served_version(base, "e", "ecommerce"), "1.0.0")
|
|
288
|
+
|
|
289
|
+
def test_a_watch_mounted_package_has_none(self):
|
|
290
|
+
base = self.serve(b'{"name": "ecommerce"}')
|
|
291
|
+
self.assertIsNone(improve.served_version(base, "e", "ecommerce"))
|
|
292
|
+
|
|
293
|
+
def test_a_server_that_cannot_be_asked_is_none(self):
|
|
294
|
+
self.assertIsNone(improve.served_version("http://127.0.0.1:9", "e", "p"))
|
|
295
|
+
self.assertIsNone(improve.served_version(None, "e", "p"))
|
|
296
|
+
|
|
297
|
+
|
|
232
298
|
class ClusterMembers(unittest.TestCase):
|
|
233
299
|
"""The improver sees each case's key. A rows golden kept in
|
|
234
300
|
`golden.path` used to reach it as no key at all."""
|
|
@@ -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.
|
|
4
|
+
version: 0.1.53
|
|
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.
|
|
4
|
+
version: 0.1.53
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# The Evaluation Loop
|
|
@@ -32,6 +32,7 @@ import pathlib
|
|
|
32
32
|
import shlex
|
|
33
33
|
import shutil
|
|
34
34
|
import sys
|
|
35
|
+
import time
|
|
35
36
|
import urllib.error
|
|
36
37
|
import urllib.request
|
|
37
38
|
from typing import Any
|
|
@@ -555,6 +556,7 @@ def build(run_dirs: list[pathlib.Path], set_dir: pathlib.Path,
|
|
|
555
556
|
for name in ("publisher.json", "eval_run.malloy"):
|
|
556
557
|
if (TEMPLATE / name).exists():
|
|
557
558
|
shutil.copy(TEMPLATE / name, out / name)
|
|
559
|
+
stamp_version(out / "publisher.json")
|
|
558
560
|
if (TEMPLATE / "notebooks").exists():
|
|
559
561
|
shutil.copytree(TEMPLATE / "notebooks", out / "notebooks", dirs_exist_ok=True)
|
|
560
562
|
if (TEMPLATE / "public").exists():
|
|
@@ -566,6 +568,27 @@ def build(run_dirs: list[pathlib.Path], set_dir: pathlib.Path,
|
|
|
566
568
|
"steps": len(steps_rows)}
|
|
567
569
|
|
|
568
570
|
|
|
571
|
+
def stamp_version(manifest: pathlib.Path) -> None:
|
|
572
|
+
"""Give this build a version of its own, higher than any earlier build's.
|
|
573
|
+
|
|
574
|
+
Publisher publishes a package from its location as the version its
|
|
575
|
+
publisher.json declares, and refuses different content under a version it
|
|
576
|
+
already holds (409). A version per build lets a rebuilt report replace the
|
|
577
|
+
one served: the highest version becomes `latest`. The patch is the time in
|
|
578
|
+
milliseconds, and above any version already in the file, so a second stamp
|
|
579
|
+
in the same millisecond still rises. Builds from different machines rely
|
|
580
|
+
on their clocks: one whose clock runs behind publishes a lower version,
|
|
581
|
+
which does not become `latest`.
|
|
582
|
+
"""
|
|
583
|
+
if not manifest.exists():
|
|
584
|
+
return
|
|
585
|
+
body = json.loads(manifest.read_text())
|
|
586
|
+
previous = str(body.get("version", "")).rsplit(".", 1)[-1]
|
|
587
|
+
floor = int(previous) + 1 if previous.isdigit() else 0
|
|
588
|
+
body["version"] = f"1.0.{max(time.time_ns() // 1_000_000, floor)}"
|
|
589
|
+
manifest.write_text(json.dumps(body, indent=4) + "\n")
|
|
590
|
+
|
|
591
|
+
|
|
569
592
|
def serving_lines(cfg: config.Config, run_dirs: list[pathlib.Path],
|
|
570
593
|
out: pathlib.Path, on_model_server: bool = False) -> list[str]:
|
|
571
594
|
"""The registration command and both URLs, which are in different path spaces.
|
|
@@ -605,8 +628,10 @@ def serving_lines(cfg: config.Config, run_dirs: list[pathlib.Path],
|
|
|
605
628
|
def register(base: str, env: str, out: pathlib.Path) -> str | None:
|
|
606
629
|
"""Register the built package on `base`, or return why it could not be.
|
|
607
630
|
|
|
608
|
-
A POST
|
|
609
|
-
rebuilt report
|
|
631
|
+
A POST publishes the build as the version stamp_version gave it, so a
|
|
632
|
+
rebuilt report becomes the package's `latest` and replaces the one it
|
|
633
|
+
served; no delete is needed first. (A server with versioning off
|
|
634
|
+
re-copies the package instead, to the same effect.)
|
|
610
635
|
"""
|
|
611
636
|
body = json.dumps({"name": out.name, "location": str(out.resolve())}).encode()
|
|
612
637
|
req = urllib.request.Request(
|
|
@@ -301,6 +301,25 @@ class RefusalsAndServing(unittest.TestCase):
|
|
|
301
301
|
self.assertEqual(json.loads(body)["location"], str(out.resolve()))
|
|
302
302
|
|
|
303
303
|
|
|
304
|
+
class AVersionPerBuild(unittest.TestCase):
|
|
305
|
+
"""A rebuilt report publishes as a new version, which becomes `latest`."""
|
|
306
|
+
|
|
307
|
+
def test_each_build_declares_a_higher_version(self):
|
|
308
|
+
tmp = pathlib.Path(tempfile.mkdtemp())
|
|
309
|
+
self.addCleanup(shutil.rmtree, tmp, True)
|
|
310
|
+
manifest = tmp / "publisher.json"
|
|
311
|
+
manifest.write_text(json.dumps({"name": "eval-run", "version": "1.0.0"}))
|
|
312
|
+
build_run_package.stamp_version(manifest)
|
|
313
|
+
first = json.loads(manifest.read_text())
|
|
314
|
+
build_run_package.stamp_version(manifest)
|
|
315
|
+
second = json.loads(manifest.read_text())
|
|
316
|
+
self.assertEqual(first["name"], "eval-run")
|
|
317
|
+
major_minor = [v["version"].rsplit(".", 1)[0] for v in (first, second)]
|
|
318
|
+
self.assertEqual(major_minor, ["1.0", "1.0"])
|
|
319
|
+
patches = [int(v["version"].rsplit(".", 1)[1]) for v in (first, second)]
|
|
320
|
+
self.assertGreater(patches[1], patches[0])
|
|
321
|
+
|
|
322
|
+
|
|
304
323
|
class Register(unittest.TestCase):
|
|
305
324
|
"""`package` registers the report on the truth server itself."""
|
|
306
325
|
|
|
@@ -138,6 +138,12 @@ TOOL_ARM_TOOLS = ("mcp__publisher__get_skill",)
|
|
|
138
138
|
# `get_status` and `list_packages` reveal the other packages on the server and
|
|
139
139
|
# are no use to an answerer given its scope in the prompt.
|
|
140
140
|
#
|
|
141
|
+
# report_api a second query path, under REST rules rather than
|
|
142
|
+
# execute_query's; its answer is not one about the model.
|
|
143
|
+
# show_report draws a panel for a person; a headless answerer gets
|
|
144
|
+
# list_reports only text, and report titles and descriptions are
|
|
145
|
+
# discovery outside get_context, the thing measured.
|
|
146
|
+
#
|
|
141
147
|
# Enumerated, because deny beats allow: naming the server (`mcp__publisher`,
|
|
142
148
|
# or `mcp__publisher__*`) removes ALL of its tools including the ones wanted,
|
|
143
149
|
# verified 2026-09-08.
|
|
@@ -146,7 +152,10 @@ ANSWER_DENIED = ("mcp__publisher__compile_model",
|
|
|
146
152
|
"mcp__publisher__search_database_schema",
|
|
147
153
|
"mcp__publisher__reload_package",
|
|
148
154
|
"mcp__publisher__get_status",
|
|
149
|
-
"mcp__publisher__list_packages"
|
|
155
|
+
"mcp__publisher__list_packages",
|
|
156
|
+
"mcp__publisher__list_reports",
|
|
157
|
+
"mcp__publisher__show_report",
|
|
158
|
+
"mcp__publisher__report_api")
|
|
150
159
|
|
|
151
160
|
# Every tool this server exposes, so the two lists above can be checked to
|
|
152
161
|
# PARTITION it rather than merely not overlap. That is the answer to the one
|
|
@@ -158,8 +167,9 @@ ANSWER_DENIED = ("mcp__publisher__compile_model",
|
|
|
158
167
|
# a mismatch here is a decision, not a lint.
|
|
159
168
|
PUBLISHER_MCP_TOOLS = ("compile_model", "execute_query", "get_agent",
|
|
160
169
|
"get_context", "get_skill", "get_status",
|
|
161
|
-
"list_packages", "reload_package",
|
|
162
|
-
"
|
|
170
|
+
"list_packages", "list_reports", "reload_package",
|
|
171
|
+
"report_api", "search_database_schema",
|
|
172
|
+
"search_malloy_docs", "show_report")
|
|
163
173
|
# The platform target: a hosted MCP server exposing the same two operations
|
|
164
174
|
# under its own names. The CLI addresses a tool as `mcp__<server>__<tool>`, so
|
|
165
175
|
# both halves are configuration -- `--hosted-mcp-server` names the server (which
|
|
@@ -176,6 +176,22 @@ def init_decision(root: pathlib.Path, reinit: bool,
|
|
|
176
176
|
f"publisher.config.json")
|
|
177
177
|
|
|
178
178
|
|
|
179
|
+
def watch_note(watch_env: str | None, seed: bool) -> str | None:
|
|
180
|
+
"""The warning for --watch-env on a start that does not seed, or None.
|
|
181
|
+
|
|
182
|
+
An environment is mounted in place only when it is first loaded from the
|
|
183
|
+
config, so on a preserved root a package already published stays a
|
|
184
|
+
version: the flag sets PUBLISHER_WATCH and changes nothing an edit
|
|
185
|
+
reaches.
|
|
186
|
+
"""
|
|
187
|
+
if not watch_env or seed:
|
|
188
|
+
return None
|
|
189
|
+
return (f" ! --watch-env {watch_env} on a preserved server root: a package "
|
|
190
|
+
f"this root already published stays an immutable version, so an "
|
|
191
|
+
f"edit never reaches it. Pass --reinit too, unless the root was "
|
|
192
|
+
f"first served with --watch-env")
|
|
193
|
+
|
|
194
|
+
|
|
179
195
|
# One ranking call, unscoped, is what kicks the embedding sync.
|
|
180
196
|
#
|
|
181
197
|
# RANKING, because the marker and the sync both hang off the ranking path: a
|
|
@@ -458,6 +474,14 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
458
474
|
"With --role, the role's mcp_port in eval.toml, as --port")
|
|
459
475
|
ap.add_argument("--allow-proxy", action="store_true")
|
|
460
476
|
ap.add_argument("--trace-retrieval", action="store_true")
|
|
477
|
+
ap.add_argument("--watch-env", metavar="ENV",
|
|
478
|
+
help="serve ENV in watch mode (PUBLISHER_WATCH): its local "
|
|
479
|
+
"packages are mounted in place, so an edit is read on "
|
|
480
|
+
"reload, which eval-improve needs. Without it each "
|
|
481
|
+
"package is a published version, which no edit "
|
|
482
|
+
"reaches. The mount is decided when the environment "
|
|
483
|
+
"is first loaded, so add --reinit to switch a server "
|
|
484
|
+
"root that is already serving")
|
|
461
485
|
ap.add_argument("--reinit", action="store_true",
|
|
462
486
|
help="drop the store and re-read publisher.config.json. "
|
|
463
487
|
"Needed after a config edit -- a new environment or a "
|
|
@@ -565,8 +589,13 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
565
589
|
env["PUBLISHER_ALLOW_PROXY_CONNECTIONS"] = "true"
|
|
566
590
|
if a.trace_retrieval:
|
|
567
591
|
env["PUBLISHER_MCP_TRACE"] = "retrieval"
|
|
592
|
+
if a.watch_env:
|
|
593
|
+
env["PUBLISHER_WATCH"] = a.watch_env
|
|
568
594
|
seed, why = init_decision(root, a.reinit, changed)
|
|
569
595
|
print(why)
|
|
596
|
+
stale_watch = watch_note(a.watch_env, seed)
|
|
597
|
+
if stale_watch:
|
|
598
|
+
print(stale_watch)
|
|
570
599
|
note = retrieval_note(env) if a.role != "truth" else None
|
|
571
600
|
if note:
|
|
572
601
|
print(note)
|
|
@@ -470,6 +470,46 @@ class Roles(unittest.TestCase):
|
|
|
470
470
|
'{"old": true}\n')
|
|
471
471
|
self.assertFalse((root / "publisher.pid").exists())
|
|
472
472
|
|
|
473
|
+
def test_watch_env_reaches_the_server_as_publisher_watch(self):
|
|
474
|
+
"""Watch mode is the one way an edit reaches a served package."""
|
|
475
|
+
d = a_set(TOML)
|
|
476
|
+
root = pathlib.Path(tempfile.mkdtemp(prefix="serve-root-"))
|
|
477
|
+
pub = pathlib.Path(tempfile.mkdtemp(prefix="serve-pub-"))
|
|
478
|
+
(pub / "dist").mkdir()
|
|
479
|
+
(pub / "dist" / "server.mjs").write_text("")
|
|
480
|
+
stuck = [sys.executable, "-c", "import time; time.sleep(60)"]
|
|
481
|
+
seen = {}
|
|
482
|
+
real_popen = serve.subprocess.Popen
|
|
483
|
+
|
|
484
|
+
def spy(cmd, **kwargs):
|
|
485
|
+
seen.update(kwargs["env"])
|
|
486
|
+
return real_popen(cmd, **kwargs)
|
|
487
|
+
|
|
488
|
+
with mock.patch.object(serve, "listening", return_value=False), \
|
|
489
|
+
mock.patch.object(serve, "alive", return_value=False), \
|
|
490
|
+
mock.patch.object(serve, "server_cmd", return_value=stuck), \
|
|
491
|
+
mock.patch.object(serve.subprocess, "Popen", side_effect=spy):
|
|
492
|
+
serve.main(["--role", "model", "--set", str(d),
|
|
493
|
+
"--server-root", str(root), "--publisher-dir", str(pub),
|
|
494
|
+
"--wait", "1", "--no-warm-retrieval",
|
|
495
|
+
"--watch-env", "evals"])
|
|
496
|
+
self.assertEqual(seen.get("PUBLISHER_WATCH"), "evals")
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
class WatchNote(unittest.TestCase):
|
|
501
|
+
"""--watch-env mounts in place only on a start that seeds the store."""
|
|
502
|
+
|
|
503
|
+
def test_a_preserved_root_is_warned(self):
|
|
504
|
+
note = serve.watch_note("evals", seed=False)
|
|
505
|
+
self.assertIn("--reinit", note)
|
|
506
|
+
self.assertIn("immutable version", note)
|
|
507
|
+
|
|
508
|
+
def test_a_seeding_start_is_not(self):
|
|
509
|
+
self.assertIsNone(serve.watch_note("evals", seed=True))
|
|
510
|
+
|
|
511
|
+
def test_no_watch_env_is_not(self):
|
|
512
|
+
self.assertIsNone(serve.watch_note(None, seed=False))
|
|
473
513
|
|
|
474
514
|
|
|
475
515
|
class WarmByDefault(unittest.TestCase):
|
|
@@ -167,22 +167,20 @@ regardless -- so check the `PUBLISHER_READY` line for the port it actually took.
|
|
|
167
167
|
Two more Publisher behaviours worth knowing, because both look like your edit did
|
|
168
168
|
nothing.
|
|
169
169
|
|
|
170
|
-
|
|
171
|
-
in `publisher.config.json`
|
|
172
|
-
|
|
173
|
-
`
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
Restarting works too, and is the only option if you would rather not reason about
|
|
185
|
-
which half is stale.
|
|
170
|
+
Unless its environment is watched, it serves a **published version**, under
|
|
171
|
+
`publisher_data/<env>/<pkg>/<version>/`. The `location` in `publisher.config.json`
|
|
172
|
+
is read once, when the version is published, and a version is immutable:
|
|
173
|
+
`reload_package` answers it `mode: unchanged` and never reads your files, and
|
|
174
|
+
neither does a restart. Never copy files into `publisher_data/` to get around
|
|
175
|
+
that. It holds the version folders, and a version's files are not read again.
|
|
176
|
+
|
|
177
|
+
To iterate, start the server with `--watch-env <env>`. The package is mounted in
|
|
178
|
+
place, and `reload_package` (`mode: in-place`) recompiles what you saved. Watch
|
|
179
|
+
`sourceContentSha` across the reload to confirm it saw the change. To ship an
|
|
180
|
+
edit to a published package, bump `version` in its `publisher.json` and publish
|
|
181
|
+
it again (`POST …/environments/<env>/packages` with its `location`): the new
|
|
182
|
+
version becomes `latest`. `eval.py package` does this for the report: each build
|
|
183
|
+
declares a higher version.
|
|
186
184
|
|
|
187
185
|
And a package must be declared in `publisher.config.json` with an `environments`
|
|
188
186
|
block -- a bare `{"frozenConfig": false}` starts cleanly and serves nothing, logging
|
|
@@ -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.
|
|
4
|
+
version: 0.1.53
|
|
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.
|
|
4
|
+
version: 0.1.53
|
|
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.
|
|
4
|
+
version: 0.1.53
|
|
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.
|
|
4
|
+
version: 0.1.53
|
|
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.
|
|
4
|
+
version: 0.1.53
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!--
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: malloy-compliance-review
|
|
3
3
|
description: 'Check a Malloy package and its controls against a written access policy: turn the policy''s prose into a control register, run compliance.py check against a served package (structure, access as each role, and the values themselves), judge what needs a person, and produce a report and a signable attestation. Use when the user asks whether a model "complies with", "satisfies" or "meets" a policy, HIPAA, SOC 2, PCI or the like, asks for a compliance review or attestation, or wants the review in CI.'
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.53
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!--
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: malloy-dashboards
|
|
3
3
|
description: "Renamed to malloy-reports. A dashboard is now a report with layout=grid: a .malloy file with an ## artifact tag, written at the package root. Load malloy-reports instead when the user asks for a dashboard, a filterable view, or drill-through between views."
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.53
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!--
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: malloy-define
|
|
3
3
|
description: Propose scope, a source plan and field definitions for a Malloy model. Which tables and questions, which sources at what grain, then renames, dimensions and measures, each backed by querying data.
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.53
|
|
5
5
|
---
|
|
6
6
|
<!--
|
|
7
7
|
Copyright (c) Credible Data Inc.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: malloy-define-agent
|
|
3
3
|
description: Define a package agent - a named brief, with its own skills, optional schedules and a run budget, declared in a package's publisher.json. Use when the user asks to create, add, change or fix an agent in a package, or wants a recurring or unattended job. Not for adopting an agent that already exists (that is malloy-run-agent), and not for a one-off question, which you just answer.
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.53
|
|
5
5
|
---
|
|
6
6
|
<!--
|
|
7
7
|
Copyright (c) Credible Data Inc.
|