@malloy-publisher/skills 0.1.23 → 0.1.24
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-answer/reference/ledger-schema.md +1 -0
- package/skills/eval-answer/scripts/init_truth_package.py +10 -0
- package/skills/eval-answer/scripts/ledger.py +1 -1
- package/skills/eval-answer/scripts/verify_goldens.py +25 -0
- package/skills/eval-answer/scripts/verify_goldens_test.py +64 -0
- package/skills/eval-diagnose/SKILL.md +1 -1
- package/skills/eval-import/SKILL.md +1 -1
- package/skills/eval-import/reference/case-format.md +1 -1
- package/skills/eval-improve/SKILL.md +1 -1
- package/skills/eval-judge/SKILL.md +25 -2
- package/skills/eval-judge/reference/writing-rubrics.md +8 -0
- package/skills/eval-loop/SKILL.md +1 -1
- package/skills/eval-loop/reference/running-a-run.md +4 -0
- package/skills/eval-loop/scripts/agent_harness.py +5 -0
- package/skills/eval-loop/scripts/agent_harness_test.py +3 -1
- package/skills/eval-loop/scripts/run_baseline.py +75 -3
- package/skills/eval-loop/scripts/run_baseline_test.py +92 -0
- package/skills/eval-report/SKILL.md +1 -1
- package/skills/malloy/SKILL.md +1 -1
- package/skills/malloy-analysis/SKILL.md +1 -1
- package/skills/malloy-analysis-pitfalls/SKILL.md +1 -1
- package/skills/malloy-analysis-report/SKILL.md +1 -1
- package/skills/malloy-analyze/SKILL.md +1 -1
- package/skills/malloy-charts/SKILL.md +1 -1
- package/skills/malloy-dashboards/SKILL.md +6 -3
- package/skills/malloy-debug/SKILL.md +1 -1
- package/skills/malloy-define/SKILL.md +1 -1
- package/skills/malloy-discover/SKILL.md +1 -1
- package/skills/malloy-document/SKILL.md +1 -1
- package/skills/malloy-getting-started/SKILL.md +1 -1
- package/skills/malloy-gotchas-modeling/SKILL.md +1 -1
- package/skills/malloy-gotchas-queries/SKILL.md +1 -1
- package/skills/malloy-gotchas-rendering/SKILL.md +1 -1
- package/skills/malloy-html-data-app-embedding/SKILL.md +3 -1
- package/skills/malloy-html-data-app-runtime/SKILL.md +1 -1
- package/skills/malloy-html-data-apps/SKILL.md +1 -1
- package/skills/malloy-lookml-review/SKILL.md +1 -1
- package/skills/malloy-materialization/SKILL.md +9 -7
- package/skills/malloy-materialization-tuning/SKILL.md +3 -2
- package/skills/malloy-model/SKILL.md +1 -1
- package/skills/malloy-model-as-you-go/SKILL.md +1 -1
- package/skills/malloy-modeling/SKILL.md +2 -2
- package/skills/malloy-notebook-chat/SKILL.md +1 -1
- package/skills/malloy-notebooks/SKILL.md +1 -1
- package/skills/malloy-patterns/SKILL.md +1 -1
- package/skills/malloy-phrase-detection/SKILL.md +1 -1
- package/skills/malloy-publish/SKILL.md +1 -1
- package/skills/malloy-queries/SKILL.md +1 -1
- package/skills/malloy-review/SKILL.md +1 -1
- package/skills/malloy-scope/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.24
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Evaluate One Answer
|
|
@@ -124,6 +124,7 @@ on that axis.
|
|
|
124
124
|
| `diagnoserModel` / `improverModel` | Written by `diagnose.py` / `improve.py` when those stages run, so the run names every LLM that touched it. Absent on a run that was only answered and judged. |
|
|
125
125
|
| `modelGitSha` | Commit of the model repo, from `--model-repo`; `-dirty` suffix when that path had uncommitted changes -- fine for a band measurement, not for an A/B pin. **Null unless `--model-repo` names the repo.** It cannot be inferred: Publisher serves a COPY under `publisher_data/`, so the tree around the served file is the server's storage, not the model's history, and the field named that until 2026-09-03. `modelSha` above is the pin that always works; this is the pointer to where those bytes are versioned, and absent beats wrong. |
|
|
126
126
|
| `modelRepo` | The path `--model-repo` named, so a reader can tell an absent pin from an unrecorded one. |
|
|
127
|
+
| `modelDir` | The package directory `--model-dir` named, when it was: the path the `-dirty` marker on `modelGitSha` was decided over. Absent means the whole repo, where any unrelated untracked file marks the model dirty. |
|
|
127
128
|
| `environment` / `package` / `modelPath` | What was served, and from where. On a platform target `environment` is the organization and `package` is the workspace the MCP URL is scoped to. |
|
|
128
129
|
| `scope` | Platform target only: the `environment/package[@version]` the answerer was told to pass as an explicit `scopes` entry on every `get_context` / `execute_query` call. A workspace can serve many packages (a personal workspace serves every package the user can read), so without this the run also measures whether retrieval picks the right package -- a different measurement. Absent means unscoped. |
|
|
129
130
|
| `targetVersion` *(pin)* | Platform target only: the published version the answers came from. Both hosted tools default an omitted version to the PINNED one, meaning whatever the workspace serves at the moment of the call, so this only describes the run when `--scope` carries it to every call. `--target-version` supplies it when the scope omits it; a scope naming a different version is refused rather than treated as an override, because a run cannot say which build answered if two pins disagree. |
|
|
@@ -23,6 +23,13 @@ WHAT IT PRODUCES
|
|
|
23
23
|
the header comment says to; the script will not guess
|
|
24
24
|
semantics, because guessed semantics are exactly what a
|
|
25
25
|
truth package exists to exclude.
|
|
26
|
+
A key that composes two tables (the top N by one
|
|
27
|
+
measure, then another measure over those rows) is written
|
|
28
|
+
as a raw per-key rollup declared in truth.malloy --
|
|
29
|
+
`source: t_x_by_key is t_x -> { group_by: key; aggregate:
|
|
30
|
+
... }` -- joined from an `extend` in the canonical query.
|
|
31
|
+
A raw aggregate by key is not semantics, and the
|
|
32
|
+
Publisher refuses a join declared inline in a query.
|
|
26
33
|
publisher.json the truth package, named, with the standard description.
|
|
27
34
|
publisher.config.json (--publisher-config) a Publisher server config
|
|
28
35
|
serving ONLY the truth package, for the second server the
|
|
@@ -133,6 +140,9 @@ def main() -> int:
|
|
|
133
140
|
"// own doc for where each comes from",
|
|
134
141
|
"// - raw-column dimensions for anything nested (a VARIANT column, a JSON",
|
|
135
142
|
"// path) that a golden query will need to group by",
|
|
143
|
+
"// - a raw per-key rollup (`source: t_x_by_key is t_x -> { group_by: key;",
|
|
144
|
+
"// aggregate: ... }`) wherever a key composes two tables, joined from an",
|
|
145
|
+
"// `extend` in the canonical query: the server refuses an inline join",
|
|
136
146
|
"// Keep column names in the warehouse's own case so a golden query reads as",
|
|
137
147
|
"// a warehouse query, not as a model query.",
|
|
138
148
|
"",
|
|
@@ -169,7 +169,7 @@ RUN_REQUIRED = {"runId", "target", "answererModel", "phase", "started"}
|
|
|
169
169
|
RUN_RECOMMENDED = {"judgeModel", "judgeVersion", "datasetVersion", "modelSha",
|
|
170
170
|
"skillsVersion", "answererManifest"}
|
|
171
171
|
RUN_OPTIONAL = {"label", "effort", "environment", "package", "modelPath",
|
|
172
|
-
"modelGitSha", "mcpUrl", "publisher", "predictionsReExecuted",
|
|
172
|
+
"modelGitSha", "modelDir", "mcpUrl", "publisher", "predictionsReExecuted",
|
|
173
173
|
"serverVersion", "diagnoserModel", "improverModel",
|
|
174
174
|
"rubricSha",
|
|
175
175
|
# The prompt the judge was SHOWN, hashed. `rubricSha` covers
|
|
@@ -218,6 +218,16 @@ def check_value(case: dict[str, Any], a: argparse.Namespace
|
|
|
218
218
|
# canonicalQuery" and count as an error, so a set with one criteria
|
|
219
219
|
# golden failed its own audit with exit 1.
|
|
220
220
|
return "skipped", "criteria: the clauses are the key, nothing to re-derive", None
|
|
221
|
+
if g.get("status") in ("ambiguous", "invalid"):
|
|
222
|
+
# A held golden is settled by a person through the golden side door
|
|
223
|
+
# and never re-derived (`promotion_blocker` says the same). Running the
|
|
224
|
+
# value check on it printed HTTP 404 on every audit for a key whose
|
|
225
|
+
# canonicalQuery read the model under test, `verify()` counted that
|
|
226
|
+
# error as drift, and `run_baseline` refused to start the arm -- with
|
|
227
|
+
# `--refresh` as the suggested remedy, which cannot fix an error.
|
|
228
|
+
return ("skipped",
|
|
229
|
+
f"{g['status']}: settled by a person through the golden side "
|
|
230
|
+
f"door, not re-derived", None)
|
|
221
231
|
if not q:
|
|
222
232
|
# A PROVISIONAL golden with no query is young, not wrong, and it is
|
|
223
233
|
# exactly what `eval-import` produces from a set that arrived as
|
|
@@ -243,6 +253,21 @@ def check_value(case: dict[str, Any], a: argparse.Namespace
|
|
|
243
253
|
rows, err = try_query(a.publisher, a.environment, a.truth_package,
|
|
244
254
|
a.truth_model, q)
|
|
245
255
|
if err:
|
|
256
|
+
if "not queryable" in err:
|
|
257
|
+
# The single most likely state of a set authored before its truth
|
|
258
|
+
# package existed: every key replays a packaged view (`run:
|
|
259
|
+
# orders -> ...`), and the truth server answers 404 "Query
|
|
260
|
+
# target is not queryable" 23 times with no hint that the query's
|
|
261
|
+
# root source is not a raw table. The message sent a reader to the
|
|
262
|
+
# server; the defect is in the golden.
|
|
263
|
+
m = re.match(r"\s*run:\s*([A-Za-z_]\w*)", q)
|
|
264
|
+
root = f"`{m.group(1)}`" if m else "its root source"
|
|
265
|
+
return ("error",
|
|
266
|
+
f"canonicalQuery reads {root}, which the truth package does "
|
|
267
|
+
f"not serve as queryable: either the truth model does not "
|
|
268
|
+
f"declare it (a key derived through the model under test, "
|
|
269
|
+
f"not the raw tables) or the query did not compile. "
|
|
270
|
+
f"Re-derive the key through the truth package", None)
|
|
246
271
|
return "error", err[:160], None
|
|
247
272
|
|
|
248
273
|
want = g.get("value")
|
|
@@ -1021,3 +1021,67 @@ class NumericRendering(unittest.TestCase):
|
|
|
1021
1021
|
|
|
1022
1022
|
if __name__ == "__main__":
|
|
1023
1023
|
unittest.main()
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
class HeldGoldensAreNotReDerived(unittest.TestCase):
|
|
1027
|
+
"""An `ambiguous` or `invalid` golden is a person's judgement. Running the
|
|
1028
|
+
value check on one reported an error on every audit, `verify()` counted the
|
|
1029
|
+
error as drift, and the arm refused to start."""
|
|
1030
|
+
|
|
1031
|
+
def setUp(self):
|
|
1032
|
+
self.a = argparse.Namespace(rewrite=False, publisher="http://x",
|
|
1033
|
+
environment="e", truth_package="t",
|
|
1034
|
+
truth_model="truth.malloy")
|
|
1035
|
+
|
|
1036
|
+
def case(self, status):
|
|
1037
|
+
return {"qid": "s-1", "golden": {"kind": "scalar", "status": status,
|
|
1038
|
+
"canonicalQuery": "run: model -> { aggregate: n }",
|
|
1039
|
+
"value": {"n": 1}}}
|
|
1040
|
+
|
|
1041
|
+
def test_a_held_golden_is_skipped_with_the_reason(self):
|
|
1042
|
+
for status in ("ambiguous", "invalid"):
|
|
1043
|
+
with self.subTest(status=status):
|
|
1044
|
+
with unittest.mock.patch.object(verify_goldens, "try_query") as tq:
|
|
1045
|
+
got, detail, rows = check_value(self.case(status), self.a)
|
|
1046
|
+
tq.assert_not_called()
|
|
1047
|
+
self.assertEqual(got, "skipped")
|
|
1048
|
+
self.assertIn(status, detail)
|
|
1049
|
+
self.assertIn("side door", detail)
|
|
1050
|
+
|
|
1051
|
+
def test_a_provisional_golden_with_a_query_still_runs(self):
|
|
1052
|
+
with unittest.mock.patch.object(verify_goldens, "try_query",
|
|
1053
|
+
return_value=([{"n": 1}], None)) as tq:
|
|
1054
|
+
got, _, _ = check_value(self.case("provisional"), self.a)
|
|
1055
|
+
tq.assert_called_once()
|
|
1056
|
+
self.assertEqual(got, "ok")
|
|
1057
|
+
|
|
1058
|
+
|
|
1059
|
+
class ANotQueryableKeyNamesItsRootSource(unittest.TestCase):
|
|
1060
|
+
"""A set authored before its truth package existed replays packaged views;
|
|
1061
|
+
the truth server answers 404 for each, and the bare message sent a reader
|
|
1062
|
+
to the server instead of the golden."""
|
|
1063
|
+
|
|
1064
|
+
def test_the_root_source_and_the_remedy_are_in_the_detail(self):
|
|
1065
|
+
a = argparse.Namespace(rewrite=False, publisher="http://x", environment="e",
|
|
1066
|
+
truth_package="t", truth_model="truth.malloy")
|
|
1067
|
+
case = {"qid": "s-2", "golden": {"kind": "scalar", "status": "provisional",
|
|
1068
|
+
"canonicalQuery": "run: orders -> { aggregate: n }",
|
|
1069
|
+
"value": {"n": 1}}}
|
|
1070
|
+
err = 'HTTP 404: {"code":404,"message":"Query target is not queryable."}'
|
|
1071
|
+
with unittest.mock.patch.object(verify_goldens, "try_query",
|
|
1072
|
+
return_value=(None, err)):
|
|
1073
|
+
got, detail, _ = check_value(case, a)
|
|
1074
|
+
self.assertEqual(got, "error")
|
|
1075
|
+
self.assertIn("`orders`", detail)
|
|
1076
|
+
self.assertIn("truth package", detail)
|
|
1077
|
+
|
|
1078
|
+
def test_any_other_error_is_reported_as_it_came(self):
|
|
1079
|
+
a = argparse.Namespace(rewrite=False, publisher="http://x", environment="e",
|
|
1080
|
+
truth_package="t", truth_model="truth.malloy")
|
|
1081
|
+
case = {"qid": "s-3", "golden": {"kind": "scalar", "status": "provisional",
|
|
1082
|
+
"canonicalQuery": "run: t_a -> { aggregate: n }",
|
|
1083
|
+
"value": {"n": 1}}}
|
|
1084
|
+
with unittest.mock.patch.object(verify_goldens, "try_query",
|
|
1085
|
+
return_value=(None, "HTTP 500: boom")):
|
|
1086
|
+
got, detail, _ = check_value(case, a)
|
|
1087
|
+
self.assertEqual((got, detail), ("error", "HTTP 500: boom"))
|
|
@@ -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.24
|
|
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.24
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Import questions into an eval set
|
|
@@ -13,7 +13,7 @@ On the case:
|
|
|
13
13
|
| field | from the source | when it is missing |
|
|
14
14
|
|---|---|---|
|
|
15
15
|
| `qid` | their id, if they gave one, prefixed with the set name | derive from the question: a slug of its first words plus a counter. Stable, because scores are keyed on it |
|
|
16
|
-
| `question` | the question text, byte for byte | it is not a case. Do not reconstruct a question from a criterion |
|
|
16
|
+
| `question` | the question text, byte for byte. One exception: a source hard-wrapped at a column width cannot be one line byte for byte, so a line break inside a question becomes one space, the seal is on the unwrapped text, and `sourceNote` says so | it is not a case. Do not reconstruct a question from a criterion |
|
|
17
17
|
| `questionSha` | never from the source | stamped at conversion by `import_cases.py --stamp` |
|
|
18
18
|
| `split` | rarely present | you choose, and freeze it at import |
|
|
19
19
|
| `source` | how it arrived: the filename, or the log query | say `unknown` rather than guessing a provenance |
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: eval-improve
|
|
3
3
|
description: 'Make the smallest safe Malloy model edit that closes a diagnosed model-owned gap, with a probe receipt for every factual claim. 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.24
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Improve the Model
|
|
@@ -1,12 +1,12 @@
|
|
|
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.24
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# The judge
|
|
8
8
|
|
|
9
|
-
JUDGE_VERSION:
|
|
9
|
+
JUDGE_VERSION: 6
|
|
10
10
|
|
|
11
11
|
This skill IS the judge. One fresh judge subagent is spawned per attempt, with
|
|
12
12
|
this skill installed in its workspace and the case materials in its prompt. It
|
|
@@ -186,6 +186,29 @@ Output, exactly this shape:
|
|
|
186
186
|
total; the question had never asked for one. A rubric that means "campaign
|
|
187
187
|
total only" must say so as `REQUIRED`, and the question should say so too.
|
|
188
188
|
|
|
189
|
+
### Rubric markers
|
|
190
|
+
|
|
191
|
+
A case rubric marks its alternate readings and disclosures with the words
|
|
192
|
+
below, and each word fixes the verdict. Apply them as written; do not re-weigh
|
|
193
|
+
a reading the rubric has already classified. (`reference/writing-rubrics.md`
|
|
194
|
+
is where authors are told to use them; this table is the judge's half.)
|
|
195
|
+
|
|
196
|
+
| Marker | Verdict | Meaning |
|
|
197
|
+
|---|---|---|
|
|
198
|
+
| `PREFERRED` | `match` | The reading the golden encodes. |
|
|
199
|
+
| `ACCEPT` | `match` | Equally right: a different but faithful route to the same claim. Check the figure against the golden the way the clause says to. |
|
|
200
|
+
| `DIVERGENT` | `near_match` | Defensible and not what was asked for. Never `no_match`, however clearly the answer committed to it. |
|
|
201
|
+
| `WRONG` | `no_match` | Plausible and incorrect; the clause usually names the trap. |
|
|
202
|
+
| `REQUIRED` | omitted: `no_match` | A disclosure without which the number misleads. |
|
|
203
|
+
| `CREDITED` | omitted: `match` | Context a good analyst adds; its absence costs nothing. |
|
|
204
|
+
|
|
205
|
+
Measured on an unchanged answer, rubric and golden: an answer whose
|
|
206
|
+
recommended figure the rubric marked `DIVERGENT` scored `near_match` under
|
|
207
|
+
one judge and `no_match` under the next, because the judge had never been
|
|
208
|
+
told what the word meant and weighed the commitment instead. An unmarked
|
|
209
|
+
clause is `CREDITED` (the author's bug, not yours to repair by inventing a
|
|
210
|
+
requirement).
|
|
211
|
+
|
|
189
212
|
### Anchors
|
|
190
213
|
|
|
191
214
|
- **match**: question "total sales by category"; golden 8 rows
|
|
@@ -40,6 +40,14 @@ Rules that follow from this:
|
|
|
40
40
|
must say "for the campaign as a whole". If it does not, the rubric must accept
|
|
41
41
|
a correct figure at any stated grain (judge rule 11). A rubric that quietly
|
|
42
42
|
assumes the golden's grain fails correct answers.
|
|
43
|
+
- **Fix the cardinality in the question, or accept any prefix in the rubric.**
|
|
44
|
+
"The most", "the top ones", "most often" name no N; the truth query needs
|
|
45
|
+
one, so the golden fixes 10 or 20 or 25 and an answer that shows five
|
|
46
|
+
correct rows reads as missing fifteen. Measured over one 24-case set, that
|
|
47
|
+
class alone produced a `no_match`, a flip and two `near_match` verdicts on
|
|
48
|
+
answers whose rows were a correct prefix of the golden's. Either write the N
|
|
49
|
+
into the question, or mark the rubric `ACCEPT the first N of the golden's
|
|
50
|
+
order for any N of at least five`, and say which order the golden is in.
|
|
43
51
|
- **A right value plus a missing `CREDITED` disclosure is a `match`.** Not a
|
|
44
52
|
near match. Do not deduct for it.
|
|
45
53
|
- **`DIVERGENT` is about definitions, not arithmetic.** A clause permitting a
|
|
@@ -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 under the model package''s evals/ directory; checkpoints are git commits of the model repo. 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.24
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# The Evaluation Loop
|
|
@@ -15,6 +15,10 @@ check, a judge, and a conformant `events.jsonl`.
|
|
|
15
15
|
# if it does not reach `ready`: the sync is lazy, so without it the first
|
|
16
16
|
# cases are answered LEXICALLY and the run reports that as the model's
|
|
17
17
|
# number.
|
|
18
|
+
# Semantic retrieval needs EMBEDDING_API_KEY in the server's environment
|
|
19
|
+
# (the Publisher does not read OPENAI_API_KEY); without it every ranking is
|
|
20
|
+
# lexical, the warm-up reports `cooldown`, and the provider's own error is
|
|
21
|
+
# only in publisher.log (grep Embedding).
|
|
18
22
|
python3 skills/eval-loop/scripts/serve.py --publisher-dir <publisher>/packages/server \
|
|
19
23
|
--server-root <root> --port 4811 --mcp-port 4040 --trace-retrieval \
|
|
20
24
|
--warm-retrieval --environment <env> --package <pkg> \
|
|
@@ -90,6 +90,11 @@ ALWAYS_BLOCKED = (
|
|
|
90
90
|
# launched detached, and they alone flagged every attempt in a 37-case run.
|
|
91
91
|
# `Task` was blocked; the newer task-management tools were not.
|
|
92
92
|
"TaskCreate", "TaskGet", "TaskList", "TaskUpdate",
|
|
93
|
+
# Granted to every headless session by the current CLI. Measured
|
|
94
|
+
# 2026-09-22 on the first smoke against a real model: it alone flagged the attempt
|
|
95
|
+
# contaminated on an answer that made two MCP calls and nothing else, and
|
|
96
|
+
# a judged arm would have voided all 24 verdicts.
|
|
97
|
+
"Artifact",
|
|
93
98
|
)
|
|
94
99
|
|
|
95
100
|
# The role-dependent half, and the reason it cannot join ALWAYS_BLOCKED:
|
|
@@ -94,9 +94,11 @@ class TheFence(unittest.TestCase):
|
|
|
94
94
|
# Each of these was measured as GRANTED to the clustering agent on
|
|
95
95
|
# 2026-09-08. `Task` spawns a sub-agent confined by nothing;
|
|
96
96
|
# `ReportFindings` cost the coverage judge its only turn.
|
|
97
|
+
# `Artifact` was measured as granted to a headless answerer on
|
|
98
|
+
# 2026-09-22; it alone flagged the attempt contaminated.
|
|
97
99
|
for tool in ("Task", "ReportFindings", "SendMessage", "Workflow",
|
|
98
100
|
"ScheduleWakeup", "RemoteTrigger", "ShareOnboardingGuide",
|
|
99
|
-
"WebFetch", "WebSearch", "ReadMcpResourceTool"):
|
|
101
|
+
"WebFetch", "WebSearch", "ReadMcpResourceTool", "Artifact"):
|
|
100
102
|
with self.subTest(tool=tool):
|
|
101
103
|
self.assertIn(tool, ah.ALWAYS_BLOCKED)
|
|
102
104
|
self.assertIn(tool, spawn_cmd(mcp_url=None))
|
|
@@ -594,7 +594,43 @@ def path_breaches(events: list[dict[str, Any]],
|
|
|
594
594
|
return list(r["reasons"])
|
|
595
595
|
|
|
596
596
|
|
|
597
|
+
# Two spellings, from two CLI paths: the persisted-output stub, and the MCP
|
|
598
|
+
# token-cap error ("result ... exceeds maximum allowed tokens. Output has been
|
|
599
|
+
# saved to <path>"). Both leave the whole payload on disk.
|
|
600
|
+
PERSISTED_STUB = re.compile(
|
|
601
|
+
r"(?:<persisted-output>.*?Full output saved to|Output has been saved to):? "
|
|
602
|
+
r"(/\S+?\.(?:json|txt))\.?(?=\s|$)", re.S)
|
|
603
|
+
|
|
604
|
+
|
|
597
605
|
def result_text(block: dict[str, Any]) -> str:
|
|
606
|
+
"""The text of one tool_result block, with a persisted stub resolved.
|
|
607
|
+
|
|
608
|
+
Above a size the CLI decides, a tool result reaches the answerer as a
|
|
609
|
+
`<persisted-output>` stub: a file path and a 2 KB preview. Measured on the
|
|
610
|
+
first arm against a real model, 14 of 74 get_context results (53 to 70 KB each) arrived that
|
|
611
|
+
way, and the answerer followed the path with Read every time. Reading the
|
|
612
|
+
stub as the payload scored those calls as zero entities delivered, which
|
|
613
|
+
is the opposite of what happened: the whole ranking was on disk. So when
|
|
614
|
+
the stub names a file that still exists, its content is the result.
|
|
615
|
+
"""
|
|
616
|
+
text = _raw_result_text(block)
|
|
617
|
+
m = PERSISTED_STUB.search(text)
|
|
618
|
+
if m:
|
|
619
|
+
path = pathlib.Path(m.group(1))
|
|
620
|
+
if path.exists():
|
|
621
|
+
try:
|
|
622
|
+
blocks = json.loads(path.read_text())
|
|
623
|
+
except (OSError, json.JSONDecodeError):
|
|
624
|
+
return path.read_text()
|
|
625
|
+
if isinstance(blocks, list):
|
|
626
|
+
return "\n".join(b.get("text", "") for b in blocks
|
|
627
|
+
if isinstance(b, dict) and b.get("type") == "text")
|
|
628
|
+
if isinstance(blocks, dict) and blocks.get("type") == "text":
|
|
629
|
+
return blocks.get("text", "")
|
|
630
|
+
return text
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def _raw_result_text(block: dict[str, Any]) -> str:
|
|
598
634
|
c = block.get("content")
|
|
599
635
|
if isinstance(c, str):
|
|
600
636
|
return c
|
|
@@ -1183,7 +1219,14 @@ _FENCE = re.compile(r"```(?:malloy)?\s*\n(.*?)```", re.S | re.I)
|
|
|
1183
1219
|
|
|
1184
1220
|
|
|
1185
1221
|
def _norm(q: str) -> str:
|
|
1186
|
-
|
|
1222
|
+
"""One spelling for one query. Malloy takes a newline OR a semicolon
|
|
1223
|
+
between clauses; the executed query (one line in the tool call) carries
|
|
1224
|
+
semicolons and the block the answer prints carries newlines, so the two
|
|
1225
|
+
never compared equal and `declared` never fired -- the harness fell through
|
|
1226
|
+
to `last_ok` and re-executed a probe. On one acceptance arm that read a
|
|
1227
|
+
model edit as a regression: the answer led with the filtered figure and
|
|
1228
|
+
the harness graded the unfiltered probe it ran afterwards."""
|
|
1229
|
+
return " ".join((q or "").replace(";", " ").split())
|
|
1187
1230
|
|
|
1188
1231
|
|
|
1189
1232
|
def _model_path_of(query: str, runs: list[dict[str, Any]]) -> str | None:
|
|
@@ -1242,6 +1285,26 @@ def pick_final_query(queries: list[str], calls: list[dict[str, Any]],
|
|
|
1242
1285
|
return queries[-1], "last", _model_path_of(queries[-1], runs)
|
|
1243
1286
|
|
|
1244
1287
|
|
|
1288
|
+
def store_events(path: pathlib.Path, events: list[dict[str, Any]],
|
|
1289
|
+
replaced_qids: set[str] | None = None) -> None:
|
|
1290
|
+
"""Write the arm's ledger, or splice a narrowed rebuild into it.
|
|
1291
|
+
|
|
1292
|
+
`--rebuild --only <qid>` re-derives the named cases and nothing else, and
|
|
1293
|
+
it used to write `events.jsonl` from scratch: a 29-case arm became a
|
|
1294
|
+
one-case arm after re-judging one case, its other 28 attempts and scores
|
|
1295
|
+
gone (the artifacts stayed, which is how it was recovered). Downstream
|
|
1296
|
+
nothing said so -- `flip_table --calibration` printed "0 flips over 1
|
|
1297
|
+
cases" and `diagnose.py` found "no diagnosable failures". A narrowed
|
|
1298
|
+
rebuild now replaces the named cases' lines and keeps the rest; a full
|
|
1299
|
+
write (`replaced_qids` None) is unchanged.
|
|
1300
|
+
"""
|
|
1301
|
+
if replaced_qids is None:
|
|
1302
|
+
ledger.write_events(path, events)
|
|
1303
|
+
return
|
|
1304
|
+
ledger.replace_events(path, keep=lambda e: e.get("qid") not in replaced_qids,
|
|
1305
|
+
new=events)
|
|
1306
|
+
|
|
1307
|
+
|
|
1245
1308
|
def retrieval_summary(attempts: Iterable[dict[str, Any]]
|
|
1246
1309
|
) -> tuple[str, dict[str, int]]:
|
|
1247
1310
|
"""Which retriever answered this run's get_context calls, and the tally.
|
|
@@ -2686,6 +2749,13 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
2686
2749
|
"Publisher serves a copy under publisher_data/, so "
|
|
2687
2750
|
"there is no way to infer it; without this the run "
|
|
2688
2751
|
"carries modelSha, the content pin, and no git pin")
|
|
2752
|
+
ap.add_argument("--model-dir", default=None, type=pathlib.Path,
|
|
2753
|
+
help="the package directory inside --model-repo the answerer "
|
|
2754
|
+
"was served from. The -dirty marker on modelGitSha is "
|
|
2755
|
+
"decided over this path, so an unrelated untracked file "
|
|
2756
|
+
"elsewhere in the repo (a scratch notebook, a run "
|
|
2757
|
+
"directory) does not stamp a clean model dirty. "
|
|
2758
|
+
"Recorded as modelDir; defaults to the whole repo")
|
|
2689
2759
|
ap.add_argument("--skills-root", default=None,
|
|
2690
2760
|
help="a checkout holding skills/ and manifests/ to load the "
|
|
2691
2761
|
"answerer's and judge's doctrine from -- a Publisher "
|
|
@@ -3198,7 +3268,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
3198
3268
|
# those bytes are versioned, and it is absent rather than wrong when
|
|
3199
3269
|
# nobody said where that is.
|
|
3200
3270
|
modelRepo=str(a.model_repo) if a.model_repo else None,
|
|
3201
|
-
|
|
3271
|
+
modelDir=str(a.model_dir) if a.model_dir else None,
|
|
3272
|
+
modelGitSha=(git_sha(a.model_repo, scope=a.model_dir or a.model_repo)
|
|
3202
3273
|
if a.model_repo else None),
|
|
3203
3274
|
skillsVersion=ledger.skills_git_sha(a.roots[0]),
|
|
3204
3275
|
skillsRoot=str(a.roots[0]),
|
|
@@ -3389,7 +3460,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
3389
3460
|
must_not_use_hits=mnu["hits"] or None,
|
|
3390
3461
|
artifactPath=f"artifacts/{qid}/judge.md"))
|
|
3391
3462
|
|
|
3392
|
-
|
|
3463
|
+
store_events(a.out / "events.jsonl", events,
|
|
3464
|
+
{c["qid"] for c in cases} if (a.rebuild and a.only) else None)
|
|
3393
3465
|
|
|
3394
3466
|
# near_match is neither a pass nor a fail: see flip_table.py. Reported on
|
|
3395
3467
|
# its own line, because a set whose near_match count is climbing has rubrics
|
|
@@ -77,6 +77,23 @@ class FinalQuery(unittest.TestCase):
|
|
|
77
77
|
rb.pick_final_query(["run: a -> answer"] * 2, calls, text)[2],
|
|
78
78
|
"answer.malloy")
|
|
79
79
|
|
|
80
|
+
def test_a_declared_query_matches_however_it_was_laid_out(self):
|
|
81
|
+
# The executed query is one line with semicolons; the answer prints the
|
|
82
|
+
# same query on several lines. Before, they never compared equal and
|
|
83
|
+
# the harness graded the probe that ran last.
|
|
84
|
+
ran = "run: a -> { where: x = 1; aggregate: n is count() }"
|
|
85
|
+
calls = [
|
|
86
|
+
{"tool": "execute_query", "query": ran,
|
|
87
|
+
"modelPath": "m.malloy", "error": None},
|
|
88
|
+
{"tool": "execute_query", "query": "run: a -> probe",
|
|
89
|
+
"modelPath": "p.malloy", "error": None},
|
|
90
|
+
]
|
|
91
|
+
text = ("The filtered total.\n\n```malloy\nrun: a -> {\n where: x = 1\n"
|
|
92
|
+
" aggregate: n is count()\n}\n```")
|
|
93
|
+
self.assertEqual(
|
|
94
|
+
rb.pick_final_query([ran, "run: a -> probe"], calls, text),
|
|
95
|
+
(ran, "declared", "m.malloy"))
|
|
96
|
+
|
|
80
97
|
def test_a_call_that_named_no_file_reports_none(self):
|
|
81
98
|
# The server resolved the file from its own default, so the run's
|
|
82
99
|
# default is the closer guess than a file another call named.
|
|
@@ -1503,6 +1520,16 @@ class GitSha(unittest.TestCase):
|
|
|
1503
1520
|
rb.git_sha(pathlib.Path("sub"), scope=pathlib.Path("sub"))
|
|
1504
1521
|
.endswith("-dirty"))
|
|
1505
1522
|
|
|
1523
|
+
def test_dirt_outside_the_scope_is_not_the_models(self):
|
|
1524
|
+
# A scratch file at the repo root: the tree is dirty, the model is not.
|
|
1525
|
+
# Scoped to the package directory the marker says so; unscoped it
|
|
1526
|
+
# stamps a clean model -dirty, which is what every run pin used to read.
|
|
1527
|
+
(self.repo / "scratch.txt").write_text("notes\n")
|
|
1528
|
+
self.assertFalse(
|
|
1529
|
+
rb.git_sha(pathlib.Path("."), scope=pathlib.Path("sub"))
|
|
1530
|
+
.endswith("-dirty"))
|
|
1531
|
+
self.assertTrue(rb.git_sha(pathlib.Path(".")).endswith("-dirty"))
|
|
1532
|
+
|
|
1506
1533
|
def test_a_relative_path_still_marks_dirt(self):
|
|
1507
1534
|
self.dirty()
|
|
1508
1535
|
self.assertTrue(
|
|
@@ -1644,3 +1671,68 @@ class OffloadedToolResult(unittest.TestCase):
|
|
|
1644
1671
|
|
|
1645
1672
|
if __name__ == "__main__":
|
|
1646
1673
|
unittest.main()
|
|
1674
|
+
|
|
1675
|
+
class NarrowedRebuildKeepsTheLedger(unittest.TestCase):
|
|
1676
|
+
"""`--rebuild --only <qid>` re-derives one case. It used to write the whole
|
|
1677
|
+
ledger from that one case, and a 29-case arm read as a 1-case arm."""
|
|
1678
|
+
|
|
1679
|
+
def setUp(self):
|
|
1680
|
+
self.tmp = pathlib.Path(tempfile.mkdtemp())
|
|
1681
|
+
self.path = self.tmp / "events.jsonl"
|
|
1682
|
+
self.old = [{"kind": "attempt", "qid": "a", "sample": 1},
|
|
1683
|
+
{"kind": "score", "qid": "a", "verdict": "match"},
|
|
1684
|
+
{"kind": "attempt", "qid": "b", "sample": 1},
|
|
1685
|
+
{"kind": "score", "qid": "b", "verdict": "no_match"}]
|
|
1686
|
+
self.path.write_text("".join(json.dumps(e) + "\n" for e in self.old))
|
|
1687
|
+
|
|
1688
|
+
def tearDown(self):
|
|
1689
|
+
shutil.rmtree(self.tmp, ignore_errors=True)
|
|
1690
|
+
|
|
1691
|
+
def test_only_the_named_cases_lines_are_replaced(self):
|
|
1692
|
+
new = [{"kind": "attempt", "qid": "b", "sample": 1},
|
|
1693
|
+
{"kind": "score", "qid": "b", "verdict": "match"}]
|
|
1694
|
+
rb.store_events(self.path, new, replaced_qids={"b"})
|
|
1695
|
+
got = [json.loads(l) for l in self.path.read_text().splitlines()]
|
|
1696
|
+
self.assertEqual(got, self.old[:2] + new)
|
|
1697
|
+
|
|
1698
|
+
def test_a_full_write_still_replaces_everything(self):
|
|
1699
|
+
new = [{"kind": "attempt", "qid": "c", "sample": 1}]
|
|
1700
|
+
rb.store_events(self.path, new, None)
|
|
1701
|
+
got = [json.loads(l) for l in self.path.read_text().splitlines()]
|
|
1702
|
+
self.assertEqual(got, new)
|
|
1703
|
+
|
|
1704
|
+
|
|
1705
|
+
class PersistedStubIsTheResult(unittest.TestCase):
|
|
1706
|
+
"""Above a size the CLI decides, a tool result reaches the answerer as a
|
|
1707
|
+
stub naming a file. Reading the stub as the payload scored 14 of 74
|
|
1708
|
+
get_context calls on one arm as zero entities delivered."""
|
|
1709
|
+
|
|
1710
|
+
def setUp(self):
|
|
1711
|
+
self.tmp = pathlib.Path(tempfile.mkdtemp())
|
|
1712
|
+
|
|
1713
|
+
def tearDown(self):
|
|
1714
|
+
shutil.rmtree(self.tmp, ignore_errors=True)
|
|
1715
|
+
|
|
1716
|
+
def block(self, text):
|
|
1717
|
+
return {"content": [{"type": "text", "text": text}]}
|
|
1718
|
+
|
|
1719
|
+
def test_the_persisted_output_stub_resolves_to_the_saved_blocks(self):
|
|
1720
|
+
saved = self.tmp / "r.json"
|
|
1721
|
+
saved.write_text(json.dumps([{"type": "text", "text": "ranked: a, b, c"}]))
|
|
1722
|
+
stub = (f"<persisted-output>\nFull output saved to: {saved}\n\n"
|
|
1723
|
+
f"Preview (first 2KB):\nranked: a")
|
|
1724
|
+
self.assertEqual(rb.result_text(self.block(stub)), "ranked: a, b, c")
|
|
1725
|
+
|
|
1726
|
+
def test_the_token_cap_spelling_resolves_to_the_saved_text(self):
|
|
1727
|
+
saved = self.tmp / "r.txt"
|
|
1728
|
+
saved.write_text("ranked: a, b, c")
|
|
1729
|
+
stub = ("Error: result (68,820 characters across 1 line) exceeds maximum "
|
|
1730
|
+
f"allowed tokens. Output has been saved to {saved}.")
|
|
1731
|
+
self.assertEqual(rb.result_text(self.block(stub)), "ranked: a, b, c")
|
|
1732
|
+
|
|
1733
|
+
def test_a_stub_whose_file_is_gone_is_left_as_it_was(self):
|
|
1734
|
+
stub = f"<persisted-output>\nFull output saved to: {self.tmp / 'gone.json'}\n"
|
|
1735
|
+
self.assertEqual(rb.result_text(self.block(stub)), stub)
|
|
1736
|
+
|
|
1737
|
+
def test_an_ordinary_result_is_unchanged(self):
|
|
1738
|
+
self.assertEqual(rb.result_text(self.block("{\"sources\": []}")), "{\"sources\": []}")
|
|
@@ -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.24
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Report a Run
|
package/skills/malloy/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: malloy
|
|
3
3
|
description: Index of all Malloy skills. Use when user asks "malloy help", "what malloy skills are available", "how do I use malloy", or needs guidance on which Malloy skill to use.
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.24
|
|
5
5
|
---
|
|
6
6
|
<!-- Copyright (c) Credible Data Inc. SPDX-License-Identifier: MIT -->
|
|
7
7
|
|
|
@@ -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.24
|
|
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-pitfalls
|
|
3
3
|
description: Checks to run on a query and its numbers before a result is presented - wrong grain, fan-out, invented field names, mismatched filters, count vs distinct. The common data-analysis pitfalls.
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.24
|
|
5
5
|
---
|
|
6
6
|
<!-- Copyright (c) Credible Data Inc. SPDX-License-Identifier: MIT -->
|
|
7
7
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: malloy-analysis-report
|
|
3
3
|
description: Combine validated Malloy queries into a notebook report or dashboard. Use when the user asks to "create a report", "build a dashboard", "combine these into a report", or wants a persistent multi-query artifact.
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.24
|
|
5
5
|
---
|
|
6
6
|
<!--
|
|
7
7
|
Copyright (c) Credible Data Inc.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: malloy-analyze
|
|
3
3
|
description: Open-ended exploration with no specific question to answer, and building views/dashboards/notebooks on an existing model. Use when the user asks "what's interesting?", "explore this data", "find insights", "look for patterns", "build a dashboard", or "create views". Not for a specific data question - answering one is `malloy-analysis`, and writing down what the answer assumed is `malloy-model-as-you-go`. For exploration start at Step 1; for views on an existing model jump to View Patterns.
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.24
|
|
5
5
|
---
|
|
6
6
|
<!--
|
|
7
7
|
Copyright (c) Credible Data Inc.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: malloy-charts
|
|
3
3
|
description: Chart selection for a Malloy view, plus the renderer reference for bar_chart, line_chart, scatter_chart, maps and layouts. For choosing a visualization or adding a chart annotation.
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.24
|
|
5
5
|
---
|
|
6
6
|
<!--
|
|
7
7
|
Copyright (c) Credible Data Inc.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: malloy-dashboards
|
|
3
3
|
description: "Build or modify a Malloy Publisher dashboard, a tagged .malloy file in a package's dashboards/ directory, with auto-rendered filter controls, a grid layout, and # drill click-through. Use when the user asks for a dashboard, a filterable operational view, or drill-through between views, and no code is wanted."
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.24
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!--
|
|
@@ -69,8 +69,11 @@ Scanned at a glance is a dashboard; read top to bottom is a notebook.
|
|
|
69
69
|
include wants `"scope": "package"`**, which recompiles every file as saved: `file` only checks the
|
|
70
70
|
one you are editing, so renaming a source in `_shared.malloy` passes it while breaking every
|
|
71
71
|
dashboard that imports it. A clean compile is not a working dashboard: some tag mistakes surface
|
|
72
|
-
at step 6, and some only when you look at the page in step 7.
|
|
73
|
-
|
|
72
|
+
at step 6, and some only when you look at the page in step 7. **A not-yet-saved dashboard wants
|
|
73
|
+
`"scope": "file"` too**, not the default: a dashboard file opens with an `import`, and the
|
|
74
|
+
default `append` scope refuses one, so the new-file case fails on the import and again on the
|
|
75
|
+
path that does not exist yet. `append` is for a fragment checked against a model that is
|
|
76
|
+
already on disk, which a dashboard file is not.
|
|
74
77
|
6. **SAVE IT, RELOAD, AND READ THE MANIFEST AND THE WARNINGS.** `reload_package`, or
|
|
75
78
|
`GET …/packages/<pkg>?reload=true`. Check the status the reload returns as well as the warnings:
|
|
76
79
|
a 424 means the package did not load and your edit is not live. **The `warnings` key is absent
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: malloy-debug
|
|
3
3
|
description: Fix Malloy compile errors and understand error messages. Use when encountering errors in .malloy files, user says "fix this error", "malloy error", "compile error", "syntax error", or sees 20+ cascading errors.
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.24
|
|
5
5
|
---
|
|
6
6
|
<!--
|
|
7
7
|
Copyright (c) Credible Data Inc.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: malloy-define
|
|
3
3
|
description: Propose a source plan and field definitions for a Malloy semantic model. Covers picking which sources to model and at what grain, then proposing the specific renames, dimensions, and measures per source, every proposal backed by querying the data.
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.24
|
|
5
5
|
---
|
|
6
6
|
<!--
|
|
7
7
|
Copyright (c) Credible Data Inc.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: malloy-discover
|
|
3
3
|
description: Silent data discovery for Malloy modeling. Used at Step 1 of the modeling workflow. Scans tables, columns, distributions, and relationships without user interaction. The agent builds an internal picture before presenting anything.
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.24
|
|
5
5
|
---
|
|
6
6
|
<!--
|
|
7
7
|
Copyright (c) Credible Data Inc.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: malloy-document
|
|
3
3
|
description: 'Add documentation with #(doc) tags to Malloy models so fields and sources are described in plain language. Use when user asks to "add documentation", "add doc tags", "document the model", or wants fields and sources described for natural-language search and discovery. For declaring parameterizable filters with #(filter), see the malloy-model skill. Filters are a runtime/modeling construct (governance, latency, correctness), not a documentation tag.'
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.24
|
|
5
5
|
---
|
|
6
6
|
<!--
|
|
7
7
|
Copyright (c) Credible Data Inc.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: malloy-getting-started
|
|
3
3
|
description: First steps for using a Malloy Publisher deployment through its MCP tools. Use when connecting to Publisher for the first time, when you do not yet know the available environments, packages, or models, or when a user asks what data they can explore. Covers verifying the server, discovering data with get_context, and running a first grounded query.
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.24
|
|
5
5
|
---
|
|
6
6
|
<!--
|
|
7
7
|
Copyright (c) Credible Data Inc.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: malloy-gotchas-modeling
|
|
3
3
|
description: Common Malloy modeling mistakes, for writing source definitions, dimensions, measures or joins - reserved words, NULL checks, date functions, type casts, field management (extend except/accept/rename vs include public/internal/private), query-based source gotchas.
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.24
|
|
5
5
|
---
|
|
6
6
|
<!--
|
|
7
7
|
Copyright (c) Credible Data Inc.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: malloy-gotchas-queries
|
|
3
3
|
description: Common Malloy query and view mistakes, for writing views, queries or notebooks - chart constraints, aggregate filters, joined field aliasing, method syntax, time truncation vs extraction.
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.24
|
|
5
5
|
---
|
|
6
6
|
<!--
|
|
7
7
|
Copyright (c) Credible Data Inc.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: malloy-gotchas-rendering
|
|
3
3
|
description: Common Malloy renderer mistakes, for adding chart annotations and formatting tags or building dashboards - tag syntax, scale rules, sparkline setup, big_value patterns.
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.24
|
|
5
5
|
---
|
|
6
6
|
<!--
|
|
7
7
|
Copyright (c) Credible Data Inc.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: malloy-html-data-app-embedding
|
|
3
3
|
description: Embed an in-package HTML data app into a host page or another application, including auto-sizing and auth. Read when embedding a Publisher page via Publisher.embed.
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.24
|
|
5
5
|
---
|
|
6
6
|
<!--
|
|
7
7
|
Copyright (c) Credible Data Inc.
|
|
@@ -12,6 +12,8 @@ SPDX-License-Identifier: MIT
|
|
|
12
12
|
|
|
13
13
|
> `Publisher.embed(selector, { src })` drops a package page into a host page as a sandboxed, auto-resizing iframe. Same-origin embeds authenticate with the browser's cookies; cross-origin embeds need a signed token.
|
|
14
14
|
|
|
15
|
+
> **A cross-origin embed also needs the server to permit it.** Publisher sends `Content-Security-Policy: frame-ancestors 'self'` by default, so a page embedded from another origin is refused by the browser and renders blank -- with nothing logged server-side, which makes it look like a broken page rather than a policy. The deployment must set `PUBLISHER_FRAME_ANCESTORS` to the host page's origin (space-separated for several, or `*` to allow any). Check this first when an embed is blank: the browser console names `frame-ancestors`.
|
|
16
|
+
|
|
15
17
|
## The host-page pattern
|
|
16
18
|
|
|
17
19
|
```html
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: malloy-html-data-app-runtime
|
|
3
3
|
description: The JavaScript that drives an in-package HTML data app - calling Publisher.query, building queries from filter state, handling results and errors. For writing the page's data code.
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.24
|
|
5
5
|
---
|
|
6
6
|
<!--
|
|
7
7
|
Copyright (c) Credible Data Inc.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: malloy-html-data-apps
|
|
3
3
|
description: Build or modify an in-package HTML data app for a Malloy Publisher package (a public/ directory the package serves). Use when the user wants a hand-authored HTML dashboard or web page backed by a package's Malloy models, with no build step.
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.24
|
|
5
5
|
---
|
|
6
6
|
<!--
|
|
7
7
|
Copyright (c) Credible Data Inc.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: malloy-lookml-review
|
|
3
3
|
description: Analyze LookML files as prior art for Malloy modeling. Used during Step 1 (DISCOVER) when .lkml files are present. Coordinates reference files that extract business logic, relationships, and curation decisions. Works with or without a database connection.
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.24
|
|
5
5
|
---
|
|
6
6
|
<!--
|
|
7
7
|
Copyright (c) Credible Data Inc.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: malloy-materialization
|
|
3
3
|
description: Add and debug Malloy Persistence materializations in a package - persist an expensive source so queries read a pre-built table. Read this whenever the user wants to materialize a source, add a persist annotation, speed up a slow source, or asks why a persist source isn't building.
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.24
|
|
5
5
|
---
|
|
6
6
|
<!--
|
|
7
7
|
Copyright (c) Credible Data Inc.
|
|
@@ -100,12 +100,14 @@ Delete the smoke file and drop its table afterward.
|
|
|
100
100
|
A gated source **can** be persisted, but only on one tier and only in one shape, and the thing to be
|
|
101
101
|
careful about is not refused by anything - you have to decide it.
|
|
102
102
|
|
|
103
|
-
- **`storage=` and `#@ preaggregate` always refuse a gated source**,
|
|
104
|
-
source
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
103
|
+
- **`storage=` and `#@ preaggregate` always refuse a gated source**, naming it. The build skips a refused
|
|
104
|
+
source, records it on the run (`metadata.refusedSources`), and builds the rest of the package. A run fails
|
|
105
|
+
on a refusal only when every authored source it targeted was refused, or when `sourceNames` named this one;
|
|
106
|
+
a refused rollup never fails it. (`#@ persist storage=<name>` is the tier that materializes into a separate
|
|
107
|
+
registered storage destination and serves from there, rather than building in the source's own connection;
|
|
108
|
+
`#@ preaggregate` stores a rollup Publisher derives from a measure you annotated with a grain, rather than a
|
|
109
|
+
source you wrote.) A rollup also groups *across* the gated column, so it could not be row-filtered afterwards
|
|
110
|
+
even in principle.
|
|
109
111
|
- **A colocated `#@ persist` (no `storage=`) is admitted** when the gate is provably the entry point's
|
|
110
112
|
**own row filter**. It is refused when the gate is reached only through a join, inherited from a base
|
|
111
113
|
the compiler cannot attribute cleanly, or does not classify as a row filter at all. The gate is found
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: malloy-materialization-tuning
|
|
3
3
|
description: Optimize a package's Malloy Persistence materializations for cost and performance using the malloy-pub CLI and the materialization history. Recommend what to persist, what to stop persisting, and how to schedule/scope it. Use when the user asks to make a package cheaper or faster, tune persistence, decide what to materialize, or review persist/schedule choices.
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.24
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!--
|
|
@@ -51,6 +51,7 @@ The history is where cost lives. Each run records its trigger, timing, and how m
|
|
|
51
51
|
In the JSON, read:
|
|
52
52
|
- `metadata.durationMs`: how long the build took.
|
|
53
53
|
- `metadata.sourcesBuilt` vs `metadata.sourcesReused`: how much work each run actually did. A run that is nearly all _reused_ is cheap; one that is nearly all _built_ every time is where cost accumulates.
|
|
54
|
+
- `metadata.sourcesRefused` and `metadata.refusedSources`: sources the eligibility gate skipped. Each one serves live on every query, so a refused source the user expected to be persisted is a finding in its own right; its entry's `message` says what to change.
|
|
54
55
|
- `metadata.trigger`: `SCHEDULER` (a cron fired it) or `ON_DEMAND`.
|
|
55
56
|
- `manifest.entries[*]`: the persisted sources: `sourceName`, `physicalTableName`, `realization`.
|
|
56
57
|
|
|
@@ -60,7 +61,7 @@ Look across several runs, not one: the pattern over the recent history (how ofte
|
|
|
60
61
|
|
|
61
62
|
Weigh rebuild cost against query benefit. Common findings:
|
|
62
63
|
|
|
63
|
-
- **Persist candidate:** an expensive, frequently-queried source that is _not_ persisted (recomputed on every query). Recommend adding `#@ persist name="…"`. Strongest when the source is a heavy aggregate/join reused by many queries and its inputs change slowly. If the source carries a `#(access_filter)` gate, check which case it is first. A gate reached only through a `join_*`, or inherited from a base it cannot be attributed to,
|
|
64
|
+
- **Persist candidate:** an expensive, frequently-queried source that is _not_ persisted (recomputed on every query). Recommend adding `#@ persist name="…"`. Strongest when the source is a heavy aggregate/join reused by many queries and its inputs change slowly. If the source carries a `#(access_filter)` gate, check which case it is first. A gate reached only through a `join_*`, or inherited from a base it cannot be attributed to, means the colocated persist is **refused** - the build skips it and it serves live - so do not recommend persisting it at all. Where the gate is provably the source's own row filter the persist is admitted, and then only recommend it alongside a freshness window (`freshness.fallback="live"`): the gating column freezes at build time, so without one a revoked row can be served under its old access decision indefinitely (see `skill:malloy-materialization`, which also covers where that window does and does not bind).
|
|
64
65
|
- **Removal candidate:** a persisted source that is cheap to compute, rarely queried, or rebuilt far more often than it is read. Recommend dropping the `#@ persist` annotation (and its table): the storage + rebuild cost is not buying anything.
|
|
65
66
|
- **Cadence mismatch:** a `SCHEDULER` cadence out of step with how fast the data changes or how long a build takes. If most scheduled runs are all-reused (nothing changed), the cron is too frequent, so loosen it. If queries routinely read stale data, tighten it. If the build's `durationMs` approaches the interval, the cadence is too aggressive.
|
|
66
67
|
- **Scope mismatch:** `scope: version` re-materializes per published version (right when versions must be isolated, e.g. a schedule); `scope: package` reuses one lineage across versions (cheaper when versions can share). A schedule _requires_ `version`. If a package carries a schedule it does not need, clearing the schedule frees it to use the cheaper package scope.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: malloy-model
|
|
3
3
|
description: Build Malloy semantic models with base source and joined source files. Use when creating or modifying .malloy files, user asks to "create a malloy model", "add dimensions", "add measures", "create a source", or any Malloy model authoring task.
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.24
|
|
5
5
|
---
|
|
6
6
|
<!--
|
|
7
7
|
Copyright (c) Credible Data Inc.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: malloy-model-as-you-go
|
|
3
3
|
description: 'After answering a data question, write down what the answer assumed so the next reader can trust the number. A field with a #(doc) in the model when you can edit it, an extend in the notebook when you can only author reports, or a stated assumption plus a Malloy snippet when you can only chat. Use after every answered question that rested on a judgment call, and whenever a question is asked against tables that have no model yet.'
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.24
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!--
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: malloy-modeling
|
|
3
3
|
description: Build semantic models with Malloy for the Malloy Publisher. Read this skill whenever the user asks about modeling data or specifically mentions Malloy.
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.24
|
|
5
5
|
---
|
|
6
6
|
<!--
|
|
7
7
|
Copyright (c) Credible Data Inc.
|
|
@@ -150,7 +150,7 @@ Never guess field names. Ground yourself with `get_context` to see the sources a
|
|
|
150
150
|
Publisher compiles each configured package at boot and serves that cached model, so a source or view you add afterwards is not queryable by name until you reload the package. The loop is:
|
|
151
151
|
|
|
152
152
|
1. **Validate** the change with `compile_model`, picking the scope that matches what you are doing:
|
|
153
|
-
- Adding a new definition or query: the default (`scope: "append"`) compiles your text in the model's namespace. Note its diagnostic positions land in the model-plus-your-text concatenation.
|
|
153
|
+
- Adding a new definition or query: the default (`scope: "append"`) compiles your text in the model's namespace. Note its diagnostic positions land in the model-plus-your-text concatenation. Append checks your text against what the model already publishes, so it refuses text that declares its own data root -- `import`, `connection.table(...)` and `connection.sql(...)` are rejected. A `source:` line handed to you by `search_database_schema` is exactly that shape, so validate it at `file` or `package` scope instead.
|
|
154
154
|
- **Editing an existing definition: `scope: "file"`**, with the whole edited file as `source`. It compiles your text AS the file (append would collide with "Cannot redefine"), and diagnostics land at the true line numbers of your text.
|
|
155
155
|
- Before saving a change other files import: `scope: "package"` with the edited file as `source` runs reload's worker compiler over every `.malloy` and `.malloynb` file against your edit, so a rename that breaks an importer surfaces now instead of at reload. Each diagnostic carries `model`, the file it points at; files hidden from discovery can appear. If `modelPath` does not exactly match an existing file, a warning says the source was treated as new.
|
|
156
156
|
2. **Save** it to the package's model file.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: malloy-notebook-chat
|
|
3
3
|
description: Steps to follow when the chat is bound to a notebook or saved report. The notebook's cells are the agent's primary context, answer from it, run its queries, and only reach for get_context when the user asks about something outside it.
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.24
|
|
5
5
|
---
|
|
6
6
|
<!--
|
|
7
7
|
Copyright (c) Credible Data Inc.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: malloy-notebooks
|
|
3
3
|
description: Create Malloy notebooks (.malloynb) for interactive dashboards and data stories. Use when user asks to "create a notebook", "make a dashboard notebook", "write a malloynb", "data story", or needs to build reports/visualizations.
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.24
|
|
5
5
|
---
|
|
6
6
|
<!--
|
|
7
7
|
Copyright (c) Credible Data Inc.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: malloy-patterns
|
|
3
3
|
description: An index of what search_malloy_docs covers - language reference, common patterns (YoY, cohorts, percent of total), rendering - for picking a topic worth searching before querying the docs.
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.24
|
|
5
5
|
---
|
|
6
6
|
<!--
|
|
7
7
|
Copyright (c) Credible Data Inc.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: malloy-phrase-detection
|
|
3
3
|
description: How to phrase search_text on a get_context call so retrieval returns the fields you need instead of a truncated catalog. Covers target-type classification and decomposition patterns.
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.24
|
|
5
5
|
---
|
|
6
6
|
<!-- Copyright (c) Credible Data Inc. SPDX-License-Identifier: MIT -->
|
|
7
7
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: malloy-publish
|
|
3
3
|
description: Package Malloy models for serving by Malloy Publisher. Use when user asks to "publish", "package", "deploy", or wants to share models with others.
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.24
|
|
5
5
|
---
|
|
6
6
|
<!--
|
|
7
7
|
Copyright (c) Credible Data Inc.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: malloy-queries
|
|
3
3
|
description: Malloy query patterns and syntax, for writing or debugging a query - dates, aggregates vs dimensions, join paths, filters, string matching, chart annotations, and the errors a SQL habit produces.
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.24
|
|
5
5
|
---
|
|
6
6
|
<!--
|
|
7
7
|
Copyright (c) Credible Data Inc.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: malloy-review
|
|
3
3
|
description: Malloy semantic-model code review. Invoke when the user asks to review, audit, or critique a `.malloy` file, a folder of Malloy models, or a GitHub PR that touches Malloy. Enforces project modeling standards and emits a navigable review file.
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.24
|
|
5
5
|
---
|
|
6
6
|
<!--
|
|
7
7
|
Copyright (c) Credible Data Inc.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: malloy-scope
|
|
3
3
|
description: Present discovery findings and propose an analytical scope before modeling. Use after inspecting a package's model and data, to classify tables and recommend an analytical focus the user can pick from.
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.24
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!--
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: malloy-source-unreachable
|
|
3
3
|
description: Work out why a source is missing from get_context, or why a query against it was refused with a 404 or a 403, and what to do next. Use when a source you expect is absent from discovery, when execute_query answers "No queryable source", when a query that used to work stops working, or when deciding whether a package's curation is hiding something you need.
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.24
|
|
5
5
|
---
|
|
6
6
|
<!-- Copyright (c) Credible Data Inc. SPDX-License-Identifier: MIT -->
|
|
7
7
|
|