@malloy-publisher/skills 0.1.13 → 0.1.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +3 -3
  2. package/package.json +1 -1
  3. package/skills/eval-answer/SKILL.md +93 -7
  4. package/skills/eval-answer/reference/coverage-limits.md +215 -0
  5. package/skills/eval-answer/reference/ledger-schema.md +50 -17
  6. package/skills/eval-answer/scripts/check_contamination.py +2 -2
  7. package/skills/eval-answer/scripts/check_coverage.py +736 -0
  8. package/skills/eval-answer/scripts/check_coverage_test.py +408 -0
  9. package/skills/eval-answer/scripts/check_must_not_use.py +201 -0
  10. package/skills/eval-answer/scripts/check_must_not_use_test.py +201 -0
  11. package/skills/eval-answer/scripts/ledger.py +50 -3
  12. package/skills/eval-answer/scripts/ledger_test.py +55 -0
  13. package/skills/eval-answer/scripts/mcp_payload.py +1 -1
  14. package/skills/eval-answer/scripts/mcp_payload_test.py +1 -1
  15. package/skills/eval-answer/scripts/verify_goldens.py +669 -37
  16. package/skills/eval-answer/scripts/verify_goldens_test.py +764 -0
  17. package/skills/eval-diagnose/SKILL.md +98 -1
  18. package/skills/eval-diagnose/reference/output-contract.md +19 -0
  19. package/skills/eval-diagnose/scripts/diagnose.py +199 -22
  20. package/skills/eval-diagnose/scripts/diagnose_test.py +215 -0
  21. package/skills/eval-import/SKILL.md +243 -0
  22. package/skills/eval-import/reference/case-format.md +139 -0
  23. package/skills/eval-import/scripts/import_cases.py +415 -0
  24. package/skills/eval-import/scripts/import_cases_test.py +335 -0
  25. package/skills/eval-improve/SKILL.md +38 -2
  26. package/skills/eval-improve/scripts/improve.py +111 -15
  27. package/skills/eval-improve/scripts/improve_test.py +181 -0
  28. package/skills/eval-judge/SKILL.md +27 -5
  29. package/skills/eval-judge/reference/refusal.md +5 -2
  30. package/skills/eval-judge/reference/writing-rubrics.md +33 -2
  31. package/skills/eval-loop/SKILL.md +76 -10
  32. package/skills/eval-loop/reference/auditing-an-answer-key.md +213 -0
  33. package/skills/eval-loop/reference/checking-the-judge.md +43 -3
  34. package/skills/eval-loop/reference/golden-side-door.md +39 -0
  35. package/skills/eval-loop/reference/measurement.md +44 -0
  36. package/skills/eval-loop/reference/running-a-run.md +79 -6
  37. package/skills/eval-loop/scripts/agent_harness.py +44 -4
  38. package/skills/eval-loop/scripts/agent_harness_test.py +378 -0
  39. package/skills/eval-loop/scripts/build_run_package.py +30 -4
  40. package/skills/eval-loop/scripts/build_run_package_test.py +167 -0
  41. package/skills/eval-loop/scripts/check_judge.py +112 -8
  42. package/skills/eval-loop/scripts/check_judge_test.py +37 -0
  43. package/skills/eval-loop/scripts/flip_table.py +150 -1
  44. package/skills/eval-loop/scripts/flip_table_test.py +158 -0
  45. package/skills/eval-loop/scripts/platform_target_test.py +196 -11
  46. package/skills/eval-loop/scripts/run_baseline.py +1243 -139
  47. package/skills/eval-loop/scripts/run_baseline_test.py +733 -0
  48. package/skills/eval-loop/scripts/serve.py +208 -4
  49. package/skills/eval-loop/scripts/serve_test.py +225 -0
  50. package/skills/eval-loop/templates/eval-run-package/README.md +3 -3
  51. package/skills/eval-loop/templates/eval-run-package/eval_run.malloy +73 -16
  52. package/skills/eval-loop/templates/eval-run-package/public/app.js +20 -2
  53. package/skills/malloy/SKILL.md +2 -2
  54. package/skills/malloy-analysis/SKILL.md +57 -8
  55. package/skills/malloy-analysis-pitfalls/SKILL.md +1 -1
  56. package/skills/malloy-analysis-report/SKILL.md +1 -1
  57. package/skills/malloy-analyze/SKILL.md +1 -1
  58. package/skills/malloy-charts/SKILL.md +1 -1
  59. package/skills/malloy-dashboards/SKILL.md +29 -21
  60. package/skills/malloy-debug/SKILL.md +1 -1
  61. package/skills/malloy-define/SKILL.md +1 -1
  62. package/skills/malloy-discover/SKILL.md +1 -1
  63. package/skills/malloy-document/SKILL.md +2 -2
  64. package/skills/malloy-getting-started/SKILL.md +34 -18
  65. package/skills/malloy-gotchas-modeling/SKILL.md +1 -1
  66. package/skills/malloy-gotchas-queries/SKILL.md +1 -1
  67. package/skills/malloy-gotchas-rendering/SKILL.md +1 -1
  68. package/skills/malloy-html-data-app-embedding/SKILL.md +1 -1
  69. package/skills/malloy-html-data-app-runtime/SKILL.md +1 -1
  70. package/skills/malloy-html-data-apps/SKILL.md +1 -1
  71. package/skills/malloy-lookml-review/SKILL.md +1 -1
  72. package/skills/malloy-materialization/SKILL.md +1 -1
  73. package/skills/malloy-materialization-tuning/SKILL.md +2 -2
  74. package/skills/malloy-model/SKILL.md +1 -1
  75. package/skills/malloy-model-as-you-go/SKILL.md +1 -1
  76. package/skills/malloy-modeling/SKILL.md +16 -16
  77. package/skills/malloy-notebook-chat/SKILL.md +1 -1
  78. package/skills/malloy-notebooks/SKILL.md +1 -1
  79. package/skills/malloy-patterns/SKILL.md +1 -1
  80. package/skills/malloy-phrase-detection/SKILL.md +26 -24
  81. package/skills/malloy-publish/SKILL.md +2 -2
  82. package/skills/malloy-queries/SKILL.md +1 -1
  83. package/skills/malloy-review/SKILL.md +1 -1
  84. package/skills/malloy-scope/SKILL.md +1 -1
package/README.md CHANGED
@@ -53,11 +53,11 @@ skill points to for detail it does not inline. Start with `malloy-getting-starte
53
53
  `malloy-review` to check Malloy for correctness.
54
54
 
55
55
  A few skills (`malloy-modeling`, `malloy-publish`, `malloy-document`, `malloy-getting-started`, and
56
- the `malloy` index) are written for a Publisher host and name Publisher's `malloy_*` MCP tools
56
+ the `malloy` index) are written for a Publisher host and name Publisher's MCP tools
57
57
  directly. The rest describe Malloy itself and refer to tools by bare name (`get_context`,
58
58
  `execute_query`, `search_malloy_docs`), because the prefix depends on the
59
- host. `search_database_schema` maps the same way if a shared skill starts using it. On Publisher those are `malloy_getContext`, `malloy_executeQuery`, `malloy_searchDocs`, and
60
- `malloy_searchDatabaseSchema`.
59
+ host. `search_database_schema` maps the same way if a shared skill starts using it. On Publisher those are `get_context`, `execute_query`, `search_malloy_docs`, and
60
+ `search_database_schema`.
61
61
 
62
62
  ## Versioning
63
63
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@malloy-publisher/skills",
3
3
  "description": "Agent skills for Malloy and the Malloy Publisher",
4
- "version": "0.1.13",
4
+ "version": "0.1.17",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: eval-answer
3
3
  description: 'Score one analytical answer against a verified golden, and score which of the entities the golden depends on retrieval delivered to the answerer. Run the contamination checklist, re-execute the submitted query yourself, then spawn a judge subagent per skill:eval-judge. Append attempt, tool_call, score, and retrieval_score events to the file ledger (reference/ledger-schema.md). Never explain the failure (eval-diagnose) or edit the model (eval-improve). Use when asked whether an answer was correct, to score a run, or to baseline a model.'
4
- version: 0.1.13
4
+ version: 0.1.17
5
5
  ---
6
6
 
7
7
  # Evaluate One Answer
@@ -36,7 +36,10 @@ The checklist. An attempt is contaminated when its log shows any of:
36
36
  `modelPath` argument on an MCP `execute_query` is NOT contamination; the
37
37
  server resolves it, the answerer never reads the file);
38
38
  3. `reported_calls` greater than `host_tool_uses` (the detectable
39
- under-report floor is reported at most total tool uses).
39
+ under-report floor is reported at most total tool uses). `host_tool_uses`
40
+ is EVERY tool use the host logged, MCP calls included; while it counted only
41
+ the non-MCP ones this comparison was true of almost every clean attempt, so
42
+ a run whose attempts predate the split cannot be checked this way.
40
43
 
41
44
  `skills/eval-answer/scripts/check_contamination.py` is a reference aid that
42
45
  mechanizes the same checklist over a JSON log; your reading of the transcript
@@ -54,10 +57,23 @@ Never score the agent's reported rows. Take its final query, execute it with
54
57
  `execute_query`, and write a prediction CSV under the run's
55
58
  `artifacts/` directory.
56
59
 
57
- `submitted: false` when there is no final query. That is not a wrong answer.
58
- No verdict can be issued (`verdict: null`, with the reason) when the attempt
59
- is not submitted, when the golden is missing, provisional, invalid, or
60
- ambiguous, or when a verified golden has no local artifact to compare.
60
+ A named view is a submitted query. `execute_query` takes either ad-hoc Malloy
61
+ or a `queryName` plus `sourceName`, and its own tool description steers an
62
+ answerer to the named form; record it as the Malloy it stands for,
63
+ `run: <source> -> <view>`, which re-executes and reads the same as any other.
64
+ Capturing only the ad-hoc form recorded an attempt that did query as
65
+ `submitted: false` with no query to re-execute, and the judge then graded prose.
66
+
67
+ `submitted: false` when there is no final query. That is not a wrong answer, and
68
+ it is not by itself a reason to withhold a verdict. No verdict can be issued
69
+ (`verdict: null`, with the reason) when the attempt produced neither a query nor
70
+ any answer text, when the golden is missing, provisional, invalid, or ambiguous,
71
+ or when a verified golden that holds a value has no local artifact to compare.
72
+ A `criteria` golden holds no value and needs no artifact: its clauses are the
73
+ whole comparison, and withholding a verdict for a missing artifact there drops
74
+ a scorable case out of the pass rate. An attempt that
75
+ wrote prose and ran nothing IS judged: against a golden holding a value, an
76
+ answer containing none of it is `no_match` however well it reasons.
61
77
 
62
78
  ## Step 3: Judge the answer
63
79
 
@@ -83,6 +99,18 @@ relevant source and field definitions from the model. It returns
83
99
  - Large row sets are still the judge's job. There is no scripted row oracle:
84
100
  a script that can pass a wrong answer is worse than none, and the rubric's
85
101
  containment and column-pairing rules are what the comparison needs.
102
+ - `golden.mustNotUse` is the exception, and it is not the judge's. It names the
103
+ similar-but-wrong field, and using one is a failure however good the number
104
+ looks, which is a question about query TEXT. Run
105
+ `scripts/check_must_not_use.py` over the final query: a named field found
106
+ there forces `no_match` and records `must_not_use_hits`, keeping the judge's
107
+ own verdict beside it as `judge_verdict`. Only a BARE name vetoes. An entry
108
+ with a connective (`weekly_active_users as a cumulative series`, `product.cost
109
+ through the order_items join`) objects to a use of the field rather than to
110
+ the field, so it goes to the judge as prose, as does an entry that names no
111
+ field at all and a path's bare leaf. A veto that fires on a correct answer is
112
+ worse than one that misses, and reading the head of `X as ...` as "ban X"
113
+ failed an answer whose only sin was showing X as an extra column.
86
114
 
87
115
  ## Step 4: Score what retrieval delivered
88
116
 
@@ -103,6 +131,64 @@ this number worth having. It uses the search terms the answerer chose, so it
103
131
  attributes a failure *within* an arm and does not compare retrieval across arms
104
132
  -- that is the engine-side `eval-retrieval` skill, which does not ship here.
105
133
 
134
+ `scripts/check_coverage.py` measures the other half, and it is worth knowing
135
+ which question each answers. Recall asks whether search surfaced the entities a
136
+ case names. Coverage asks whether the model holds the concepts at all, decided
137
+ by READING the model: no answerer, no judge, no goldens, no warehouse. A version
138
+ can score full recall on a case the model could never have answered, and then
139
+ recall is scoring search against an expectation that was never satisfiable.
140
+
141
+ Because it runs from the model text alone it is cheap enough to point at every
142
+ published version and read as a trend, which is what it is for. Its verdicts are
143
+ `eval-diagnose`'s codes verbatim, validated against that table at startup, so
144
+ `COVERAGE`, `AMBIGUOUS`, `NO-DISAMBIG` and `CONVENTION` mean there exactly what
145
+ they mean here. It writes nothing: not a golden, not the case-level `coverage`
146
+ field, not a run directory. Where its verdict and that field disagree is where a
147
+ version regressed, and the field is the standing judgement about the question
148
+ while this is a measurement against one build.
149
+
150
+ How to invoke it. Either `--model <file-or-dir>` for a local package or
151
+ `--publisher <url> --package <pkg>` for a served one, plus `--set <dir>`, and
152
+ `--version <label>` to stamp the report so a trend has an x-axis:
153
+
154
+ ```
155
+ python3 check_coverage.py --set evals/ecommerce --model model.malloy --version 0.0.58
156
+ ```
157
+
158
+ Two flags change what the number means, so choose them rather than inheriting
159
+ them. `--repeat N` samples each case N times and takes the majority; it defaults
160
+ to **1 for a set**, because the score is a trend over many cases rather than a
161
+ verdict on one, and to 3 for `--self-check`, where a single fixture is the whole
162
+ measurement. A case that flips between samples is arguable rather than covered,
163
+ so a tie goes to the gap, and two DIFFERENT gaps tying leaves the case undecided
164
+ and out of the denominator. `--self-check` runs the shipped fixture instead of a
165
+ set, which is how to confirm the checker still detects a gap it is known to
166
+ detect; run it after editing the prompt, the verdict vocabulary **or the fixture
167
+ itself**. The tests pin the fixture's shape and cannot pin its verdict, because
168
+ the judge needs a live model, so a fixture swap that goes unmeasured leaves the
169
+ whole metric unguarded.
170
+
171
+ Undecided cases are excluded from the percentage and reported on their own line.
172
+ Read that line: a coverage number over a handful of decided cases is not a
173
+ measurement, it is a sample size.
174
+
175
+ **Read `reference/coverage-limits.md` before quoting a coverage number.** Run
176
+ against all 49 hand-labelled cases of the `evals/ecommerce` set it disagreed
177
+ with the authored label on 17, while the two headline percentages landed two
178
+ points apart because the disagreements cancel. Checking each one against the
179
+ model found the LABEL was the stale side more often than the verdict was: four
180
+ notes call a measure missing that the model declares, and one prescribes a
181
+ filter on a field the model documents as "not evidence of a sale". So read a
182
+ coverage number as a rough signal over many cases, never as a verdict on one and
183
+ never as a small movement between versions, and use `--compare-labels` to print
184
+ the disagreements and triage them by reading the model. Agreement with the
185
+ labels is not a score for the checker; the two answer different questions on
186
+ purpose. Separately, the whole model goes into one prompt per case, so past a
187
+ few thousand lines it stops running on Linux and above about 100 KB the verdict
188
+ stops being stable. Do not take the over-size message's advice to narrow
189
+ `--model` to one file, which drops every imported source and manufactures
190
+ `COVERAGE` verdicts.
191
+
106
192
  ## Step 5: Distrust the golden
107
193
 
108
194
  A reference answer can be wrong (parent-column fanout, a join on a shared
@@ -121,7 +207,7 @@ event unchanged, and where it says nothing, fall back to the case's standing
121
207
  Do not encode a rewrite of a bad golden into the model. A `suspect` or
122
208
  `verified_wrong`, or a no_match whose why indicts the golden rather than the
123
209
  prediction, routes to the golden side door in `skill:eval-loop` as a **dataset**
124
- issue. It is never a model failure, and it must be settled before improve runs --
210
+ issue, which is where the audit procedure for settling it lives. It is never a model failure, and it must be settled before improve runs --
125
211
  otherwise a modelling agent is dispatched to fix a model that is already right.
126
212
 
127
213
  ## Step 6: Append events, then stop
@@ -0,0 +1,215 @@
1
+ <!-- What a coverage number does and does not measure. Read before quoting one. Measured against a labelled set and across model sizes; none of it is visible in the output the script prints. -->
2
+
3
+ # Coverage limits
4
+
5
+ `check_coverage.py` was measured against a real labelled set and at a range of
6
+ model sizes. Every number below is measured, and the method is named beside it
7
+ so it can be re-measured when the harness changes. None of it is visible in the
8
+ output the script prints today.
9
+
10
+ The short version. **Read it as a rough signal over many cases, never as a
11
+ verdict on one, and never as a small movement between versions.** Where it
12
+ disagrees with a set's authored `coverage` label, either side can be the stale
13
+ one, and on the one set measured the LABEL was wrong more often than the verdict
14
+ was. Separately, the whole model goes into one prompt per case, which caps the
15
+ package size it can run on at all and destabilises the verdict well before that
16
+ cap.
17
+
18
+ ## Measured against a labelled set: the label drifts too
19
+
20
+ The `evals/ecommerce` set in `credibledata/malloy-samples` labels every case
21
+ `covered`, `derivable` or `absent` by hand, and names the entity that would
22
+ close each one. Running all 49 cases against that package's model, `--repeat 1`,
23
+ 19,258 chars of model text over three files:
24
+
25
+ | | |
26
+ |---|---|
27
+ | script | coverage 45%, 22 of 49 |
28
+ | set, counting `covered` | 43%, 21 of 49 |
29
+ | cases where the two disagree | 17 |
30
+
31
+ **Read the disagreements, not the two-point gap.** The headlines land close
32
+ because the two directions of disagreement nearly cancel, 8 cases the script
33
+ calls a gap that the label calls covered against 9 the other way. A number that
34
+ agrees for that reason can move without the model changing and hold still when
35
+ it does.
36
+
37
+ **And do not assume the checker is the wrong side.** Every disagreement was
38
+ checked against the model by hand. On the majority the LABEL was stale or
39
+ wrong, because the model gained measures and the standing judgement was not
40
+ revisited:
41
+
42
+ | case | label says | the model actually declares |
43
+ |---|---|---|
44
+ | `ecom_time_to_ship` | "no measure; shipped_at - created_at" | `avg_days_to_ship is avg(days_to_ship)`, documented |
45
+ | `ecom_gross_margin_pct` | "average_gross_margin is per item, not margin / sales" | `margin_rate is total_gross_margin / nullif(total_sales, 0)` |
46
+ | `ecom_top_margin_pct_categories` | "no margin-pct measure to group by category" | the same `margin_rate`, groupable |
47
+ | `ecom_return_rate` | "no return_rate measure, the model has no rate measure at all" | `percent_purchases_returned`, a returned-over-all ratio |
48
+
49
+ The reverse direction is the same story with the sides swapped.
50
+ `ecom_unsold_inventory` is labelled `covered` with the note
51
+ "`inventory_item_count`, filtered to unsold", and the checker returns a gap. The
52
+ checker is right: the model's doc on `sold_at` says "Despite the name it is not
53
+ evidence of a sale: it is a copy of order_items.created_at and is set even when
54
+ that order was cancelled", and `inventory_items` declares no join back to
55
+ `order_items`, so the anti-join the question needs cannot be expressed at all.
56
+ The note prescribes exactly the filter the model warns against.
57
+
58
+ So the honest reading of this run is that it audited the SET as much as the
59
+ model, and that a coverage judgement is only as good as the docs it reads.
60
+ `--compare-labels` exists for this: it joins the verdicts against the authored
61
+ field and prints the disagreements with their notes, taking no position on which
62
+ side is wrong. Check the label first, against the model, before treating a
63
+ disagreement as a coverage regression.
64
+
65
+ One number does hold up on its own. `absent` came back `COVERAGE` 4 times out
66
+ of 4, so where the model holds nothing at all the verdict is dependable. That is
67
+ also the easiest call in the set.
68
+
69
+ Do NOT read agreement with these labels as a score for the checker. The two
70
+ answer different questions on purpose, which the script's own docstring says,
71
+ and this run is the evidence that either side can be the one that has gone
72
+ stale.
73
+
74
+ ## It stops RUNNING at about 3,500 lines, on Linux only
75
+
76
+ Measured separately, on model size rather than on accuracy. Density first,
77
+ measured by concatenating example packages and reading `usage` off a real judge
78
+ call: Malloy text runs **2.70 chars per token** and **37 chars per line**, so a
79
+ 10,000-line model is about 370 KB and 136,000 tokens.
80
+
81
+ The prompt goes in `argv`. Linux caps a single argument string at
82
+ `MAX_ARG_STRLEN`, 131,072 bytes, independently of `ARG_MAX`; measured in
83
+ `python:3.12-slim`, 131,000 bytes execs and 131,072 raises `OSError 7 Argument
84
+ list too long`. macOS has no per-argument cap below its roughly 1 MB total, so
85
+ the same model that works on a developer's laptop fails in CI and in a Linux
86
+ checkout.
87
+
88
+ `MAX_PROMPT` is 400,000, so it does not catch this: three times the real Linux
89
+ ceiling, and it counts CHARACTERS while `argv` spends UTF-8 BYTES, so a model
90
+ with non-ASCII doc comments passes the guard by a wider margin still. Anything
91
+ over roughly **3,500 lines** clears the guard and then fails the exec.
92
+
93
+ The failure is shaped like a different bug. `run_cli` retries on `no_text`, so a
94
+ deterministic `E2BIG` is retried with the full backoff and lands as `no reply
95
+ from the model ([Errno 7] Argument list too long)`, once per case, which is what
96
+ a rate limit also looks like. Every case comes back undecided, `coverage` prints
97
+ `n/a`, and the process still **exits 0**.
98
+
99
+ ## It stops being STABLE at about 100 KB, everywhere
100
+
101
+ The shipped fixture exists to prove the measurement judges expressibility rather
102
+ than matching the question's words against field names. Growing only the
103
+ haystack around it, needle and prompt untouched:
104
+
105
+ | model text | verdict | |
106
+ |---|---|---|
107
+ | 2.8 KB, the fixture of the day | `CONVENTION` | correct |
108
+ | 103 KB | `ok` | WRONG, reproduced at `--repeat 5` |
109
+ | 373 KB | `CONVENTION` | correct |
110
+
111
+ **These three rows predate the current fixture and have not been re-measured.**
112
+ They were taken against a 2.8 KB excerpt that was replaced, when a customer
113
+ model was scrubbed out of `check_coverage.py`, by the 1.4 KB `support_desk`
114
+ fixture that ships today. What survives the swap is the SHAPE of the curve --
115
+ the needle was untouched in all three rows and only the haystack grew -- so read
116
+ the 100 KB cliff as the finding and the specific verdicts as history. The
117
+ shipped fixture on its own reads `CONVENTION` 3 of 3 (2026-09-08, sonnet).
118
+
119
+ At 103 KB the fixture reads `ok`, precisely the field-name matching it was
120
+ written to detect, and the majority of five samples did not rescue it. The
121
+ pattern is instability rather than decay: above roughly 100 KB the verdict stops
122
+ being a function of the model's content, and it drifts toward `ok`, the
123
+ optimistic direction `majority()` is written to avoid. 103 KB is a quarter of
124
+ `MAX_PROMPT` and under a third of a 10,000-line model, so a package can sit
125
+ inside every limit the script enforces and still produce a number that means
126
+ nothing.
127
+
128
+ One caveat on method: the haystack was example packages repeated with renamed
129
+ sources, so this measures dilution and recall rather than a genuinely distinct
130
+ 200-file package. Treat the direction as established and the threshold as
131
+ approximate.
132
+
133
+ ## What it costs
134
+
135
+ The model text and the question share ONE user message block, so the question,
136
+ which changes per case, sits inside the cached prefix and the model is re-cached
137
+ every call. Measured on two calls with the same 100 KB model and different
138
+ questions: `cache_creation_input_tokens` 37,033 and `cache_read` 0 for the
139
+ model. Moving the model into `--append-system-prompt` and leaving only the
140
+ question in the message gave `cache_read` 50,754 and `cache_creation` 1,159 on
141
+ the second call.
142
+
143
+ | per case | as written | model cached |
144
+ |---|---|---|
145
+ | 19 KB model (the ecommerce set) | $0.08 | $0.05 |
146
+ | 100 KB model | $0.181 | $0.016 |
147
+ | 370 KB model, 10k lines | $0.55 | $0.08 |
148
+
149
+ The 49-case run above cost about $5.72 and took about 11 minutes at
150
+ `--parallel 8`. The script reports neither, which for a measurement sold on
151
+ being cheap is its own gap: nothing in the output says what the number spent.
152
+
153
+ ## Do NOT narrow to one file to get under the cap
154
+
155
+ The over-size message suggests pointing `--model` at a single `.malloy` file. Do
156
+ not, and do not read a coverage number produced that way. `model_text()`
157
+ concatenates every `*.malloy` under the tree and does **not** resolve `import`;
158
+ real packages split across files exactly that way (in
159
+ `credibledata/malloy-samples`, `ecommerce/brand_synergy.malloy` opens with
160
+ `import "ecommerce.malloy"`). Narrowing to one file drops every imported source,
161
+ so the judge correctly answers `COVERAGE`, no entity represents the concept
162
+ anywhere, for cases the model answers perfectly well. That is a false gap
163
+ indistinguishable from a real one, and it lands in the numerator of a published
164
+ trend.
165
+
166
+ If the model does not fit, the honest outcome is a failed measurement, not a
167
+ measurement of a fragment.
168
+
169
+ ## Where to take it
170
+
171
+ Ordered by what each fixes.
172
+
173
+ **Run `--compare-labels` whenever a set carries authored `coverage` labels, and
174
+ triage what it prints.** It is the cheapest audit available on both the set and
175
+ the model, and it needs no extra model calls beyond the run itself. Resolve each
176
+ disagreement by reading the model, and expect to fix labels as often as
177
+ verdicts. What it must NOT become is a score for the checker: agreement is not
178
+ the goal, since the two answer different questions on purpose.
179
+
180
+ **Do not add a prompt rule on this evidence.** The first reading of that run
181
+ was that the checker had two defects, a named measure plus a filter reading as
182
+ a gap and the prompt's ratio rule going unapplied. Checking each case against
183
+ the model dissolved both: the model documents `sold_at` as NOT evidence of a
184
+ sale and declares no join for the anti-join, and the measures the labels called
185
+ missing exist. A prompt rule written from that first reading would have taught
186
+ the judge to ignore a caveat the model states plainly.
187
+
188
+ **Pass the prompt on stdin.** `claude -p` reads it there (verified), which
189
+ removes the `argv` ceiling outright. Then `MAX_PROMPT` should bound bytes
190
+ against the agent model's context window rather than characters against a Linux
191
+ limit it does not match.
192
+
193
+ **Exit 1 when `decided` is 0.** Done: a run that decided nothing now exits 1
194
+ and says so, instead of reporting `n/a` and a success. This is the whole-set
195
+ shape of the `argv` failure above, where every case fails identically.
196
+
197
+ **Note that the two cheap fixes pull against each other.**
198
+ `--append-system-prompt` is what buys the caching above, and it is also `argv`,
199
+ so on Linux it is stuck under the same 131,072 bytes. There is no
200
+ `--append-system-prompt-file`. Both cannot be had from this CLI surface, which
201
+ is the argument for not making this judge a `claude -p` call at all: it holds no
202
+ tools, is granted a second turn only to absorb a stray tool call, and scans
203
+ braces out of prose because the reply is not structured. A cached system block
204
+ for the model, a volatile user block for the question, and a structured output
205
+ format address the cost, the ceiling and `json_objects()` together. Stdlib
206
+ `urllib` keeps the stdlib-only promise; the real cost is auth, since `claude -p`
207
+ rides the CLI's credentials.
208
+
209
+ **And for a large package, stop sending the whole model.** Rule out `COVERAGE`
210
+ against a complete inventory of names and one-line docs, a small fraction of the
211
+ bytes, which keeps "no candidate anywhere" decidable; then send full text for
212
+ the candidate sources only, to settle `AMBIGUOUS`, `NO-DISAMBIG` and
213
+ `CONVENTION`. Deliberately NOT retrieval ranking: folding retrieval into this
214
+ number would collapse the distinction from recall that the metric exists to
215
+ draw.
@@ -65,8 +65,9 @@ Rules that make the ledger trustworthy:
65
65
  | `description` | |
66
66
  | `datasetVersion` | Integer. Bump on any golden repair or case change. Runs record the version they scored against. |
67
67
  | `targetModelPath` | Model path within the package. |
68
- | `truthPackage` | Name of the package holding the semantics-free sources every golden is re-derived from -- a package NAME, not an object. `verify_goldens.py` skips every check without it, reporting "nothing to re-derive against", so a set that omits it silently has no golden verification at all. `init_truth_package.py` scaffolds the package. |
68
+ | `truthPackage` | Name of the package holding the semantics-free sources every golden is re-derived from -- a package NAME, not an object. Without it `verify_goldens.py` re-derives nothing: it reports "nothing to re-derive against" and **exits 3**, which a caller must read as unverified rather than as a pass. The checks that need no truth server -- rubric figures, verification axis, stale rubric claims, set names -- still run and still report. `init_truth_package.py` scaffolds the package. |
69
69
  | `truthModel` | Model file inside that package. Default `truth.malloy`. |
70
+ | `targetPackage` | Name of the package UNDER TEST. `verify_goldens.py` refuses a "truth" server that also serves it, because an answerer on that server can retrieve the raw truth sources beside the model and quietly change what is under test. `run_baseline.py` and `improve.py` pass the package they were given, so this field is the fallback for a direct call; without either the guard cannot fire. |
70
71
  | `truthTableRewrite` | Boolean, default false. Rewrites `duckdb.table('data/x.parquet')` refs to bare `x` in canonical queries, for a truth package whose tables are registered rather than read from files. |
71
72
 
72
73
  ## `cases.jsonl`
@@ -81,7 +82,8 @@ One JSON object per line:
81
82
  | `tags` | list |
82
83
  | `state` | `candidate` / `selected` / `excluded`. |
83
84
  | `source` | Where the case came from. |
84
- | `golden` | `status` (`verified` / `provisional` / `invalid` / `ambiguous`), `kind`, `value` or `path` (artifact under the set directory), `canonicalQuery` (runs against the **truth** package, never the model under test), `verifiedBy`, and `verification` -- `{primaryAxis, variesAxis, note}` naming what the second derivation varied. **A golden is written only after two differently shaped derivations agree** (the truth query and a second one that varies a different axis -- group by something else, sum a different way, take a different route through the raw tables); `gold/<qid>.json` holds both (`truthRows`, `verifyRows`, `agreement`). Two derivations that vary the same axis share every other blind spot: that is how a golden that summed three overlapping time slices, 3x too high, passed its check. `verify_goldens.py` re-derives the value, reads the accepting clause of the rubric against the rows, and flags a second derivation that names no axis. **The question asks for data, never for an interpretation of it**, and either fixes the grain or the rubric accepts a correct figure at any stated grain (`judge.md` rules 9 and 10). |
85
+ | `golden` | `status` (`verified` / `provisional` / `invalid` / `ambiguous` / `verified_wrong`, the last being a key a person has established is wrong, which `run_baseline.py` drops from every aggregate), `kind`, `value` or `path` (artifact under the set directory), `canonicalQuery` (runs against the **truth** package, never the model under test), `verifiedBy`, and `verification` -- `{primaryAxis, variesAxis, note}` naming what the second derivation varied. **A golden is written only after two differently shaped derivations agree** (the truth query and a second one that varies a different axis -- group by something else, sum a different way, take a different route through the raw tables); `gold/<qid>.json` holds both (`truthRows`, `verifyRows`, `agreement`). Two derivations that vary the same axis share every other blind spot: that is how a golden that summed three overlapping time slices, 3x too high, passed its check. `verify_goldens.py` re-derives the value, reads the accepting clause of the rubric against the rows, and flags a second derivation that names no axis. **The question asks for data, never for an interpretation of it**, and either fixes the grain or the rubric accepts a correct figure at any stated grain (`skill:eval-judge` rules 10 and 11). |
86
+ | `golden.kind` | What the key IS. A value (`scalar`, rows), `unanswerable`, or `criteria`: prose with no value, where the author's criteria are the whole key. `criteria` is the one kind that is legitimately `verified` on arrival, because there is no derivation to perform and nothing that can be numerically wrong; an imported key holding a VALUE is `provisional` until re-derived (`skill:eval-import`). **`verify_goldens.py --promote` is the only thing that writes `golden.status`**: it marks a `provisional` golden `verified` when its value re-derived cleanly from the truth package AND a second derivation exists. `--refresh` rewrites a drifted VALUE and never changes a status; `invalid` and `ambiguous` are settled by a person through the golden side door. |
85
87
  | `golden.rubric` | **On the golden, not on the case.** The prose the judge is shown as `CASE RUBRIC`, naming what counts as correct and what does not. `run_baseline.py` reads `golden.rubric`; a rubric written at the case's top level is silently not passed, the judge is told `CASE RUBRIC: none`, and it then scores from the golden value and the answer alone -- which reads as a judge that ignores its instructions. |
86
88
  | `golden.mustState` | **On the golden, not on the case.** What the answer has to say out loud. Read from `golden.mustState` by the run-package builder. |
87
89
  | `goldenRevision` | Integer. Bump on every golden change; `score` events stamp the revision they compared against. |
@@ -119,20 +121,27 @@ on that axis.
119
121
  | `skillsVersion` *(pin)* | HEAD of the checkout the agents' skills were loaded from, dirty-marked (`ledger.skills_git_sha(root)`). The skills are the doctrine the agents load; a run that cannot name their version cannot anchor a skills A/B. |
120
122
  | `skillsRoot` / `harnessVersion` | Which checkout supplied the doctrine (`--skills-root`, e.g. a Publisher checkout for the open-source skills; default this one), and this checkout's own HEAD. The eval-* skills always come from the harness checkout, whatever `skillsRoot` says. Two runs whose `skillsRoot` differ are a skills A/B only if their manifests name the same skills. |
121
123
  | `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. |
122
- | `modelGitSha` | Commit of the model repo, when the target served working files; `-dirty` suffix when the tree had uncommitted changes -- fine for a band measurement, not for an A/B pin. Omit for a platform target, where a local commit pins nothing. |
124
+ | `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. |
125
+ | `modelRepo` | The path `--model-repo` named, so a reader can tell an absent pin from an unrecorded one. |
123
126
  | `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. |
124
- | `scope` | Platform target only: the `environment/package` 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. |
127
+ | `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. |
128
+ | `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. |
125
129
  | `mcpUrl` / `publisher` | The endpoints the answerer and the re-execution used. |
126
- | `predictionsReExecuted` | Whether the judge saw re-executed rows (false when the served bytes no longer match the pinned snapshot). |
130
+ | `predictionsReExecuted` | Whether re-execution was POSSIBLE: the server was serving the bytes this run is pinned to. It is one bool for the whole run and says nothing about any given query, so it reads stronger than it is; `reExecution` below is what actually happened. |
131
+ | `reExecution` | `{attempted, ok, failed, noQuery, notReExecuted, notJudged, missing}` over the run's predictions. `failed` means the query ran and the server returned an error, which is a different thing from an answer with no query at all. `notJudged` is a case the judge was never handed: a golden refused before judging (`provisional`, `invalid`, `ambiguous`, `verified_wrong`, missing), an attempt that submitted nothing, or every case under `--no-judge`. Those used to land in `missing` and read as corrupt artifacts on a freshly imported set. `ok`, `failed`, `noQuery`, `notReExecuted`, `notJudged` and `missing` partition the cases, so they sum to the case count; `attempted` is `ok + failed` and sits outside that sum. |
132
+ | `retrievalMode` | Which retriever answered: `semantic`, `lexical`, `mixed` (it changed mid-run, so nothing can be averaged across it) or `unreported` (nothing ranked, which on a configured server means no embedding provider). Only a RANKING call names a retriever, so a call that enumerates a type or looks a name up is `unreported` on a fully semantic server and must not make the run read as `mixed`. Local retrieval degrades to lexical SILENTLY without an embedding key, and a pair compared across that reads as a model change, so `flip_table.py` refuses the pair unless both sides match. |
133
+ | `retrievalCalls` | `{semantic, lexical, unreported}` call counts behind `retrievalMode`. `unreported` counts the calls that did not rank, which is a different thing from calls that fell back. |
134
+ | `retrievalGate` | What the pre-arm retrieval gate found, or why it did not run. A run is held until `get_context` answers semantic twice in a row, because the first calls after a restart are lexical by design and an arm started immediately measures two retrievers. `not run (no answering phase)` under `--rebuild`/`--rejudge`, and `skipped by --no-retrieval-gate` when the operator opted out -- which is a different fact from a gate that passed, and the field is what tells them apart. |
127
135
  | `label` | `<set>-<phase>-<nn>`, assigned by `run_baseline.py` from the set name, the run's phase and the next free number beside it (`ecommerce-baseline-01`, `ecommerce-baseline-02`, `ecommerce-blind_gate-01`). The A/A pair is two runs of the same phase; the post-edit arms are two runs of `blind_gate`. Hand-typed names do not survive one afternoon of runs -- `base`, `rejudged2`, `r3`, `post1` sort wrongly, group not at all, and cannot be matched to an arm. `--label` overrides for a run that genuinely needs a human name. |
128
136
  | `effort` | |
129
137
  | `answererCostUsd` / `judgeCostUsd` | What the arm cost, split by role. The judge's half was discarded until 2026-09-02, so every "cost per arm" quoted before then was the answerer alone. |
130
- | `goldenCheck` | What `verify_goldens.py` said before the run started: `N ok, M drifted, K other finding(s)`, or why it did not run (no truth server on a platform target; `--skip-golden-check`; rebuild). A run that started on a drifted set says so here rather than pretending its verdicts mean something. |
138
+ | `goldenCheck` | What `verify_goldens.py` said before the run started: `N ok, M drifted, K other finding(s)`, or why it did not run (no truth server on a platform target; `--skip-golden-check`; rebuild). A run that started on a drifted set says so here rather than pretending its verdicts mean something. `run_baseline` calls `verify()` without a model, so the set-name audit (check 5) does not run in-arm; the trailing `N stale entity name(s)` is the in-arm lint's own count, and `entity-name lint not run` says there was no model text to lint against. Without it the field read `0 other finding(s)` for a check that never happened. |
139
+ | `staleEntityNames` | The `expectedEntities` names that appear nowhere in the served model, from the in-arm lint. Each one depresses the run's own recall on every case listing it, so a stale set reads as a retrieval failure until someone checks by hand. `null` when there was no model text to lint against (a platform target), which is a different fact from `[]`. |
131
140
  | `status` | `complete`, or `aborted` when four consecutive attempts errored or found the server dead and the harness stopped rather than spend the rest of the budget on attempts nobody will trust. |
132
141
  | `packageSha` / `servedRevision` *(pins)* | Taken from the server, not recomputed: `sourceContentSha` is a content hash over EVERY model path in the package, so an edit to an imported file moves it where a sha of the one `--model-path` does not; `servedRevision` is minted per load, so it identifies a load rather than content and is a poor pin alone. Measured: `publisher.json`'s `version` moves neither, and nothing in Publisher reads it -- it is not in the Package API schema and never returned, so it pins nothing. |
133
142
  | `datasetSha` *(pin)* | Content hash of `set.json` + `cases.jsonl`. Deliberately SEPARATE from the model's sha: a golden repair is not a model change, and one pin covering both would make every answer-key fix read as an edit to the model, which is the distinction an A/B rests on. Automatic, so nobody has to remember it; `datasetVersion` stays beside it as the human-readable sequence. **Local targets only.** A hosted target that publishes IMMUTABLE versions needs none of this: the set rides inside the version, and immutability -- not hashing -- is what makes a pin trustworthy. There, `targetVersion` alone identifies model and set together. |
134
- | `doubtedGoldens` | The cases whose golden the judge did not believe: `qid`, `gold_status` (`suspect` or `verified_wrong`), `gold_note`. **Read this before diagnose.** These are dataset issues, not model failures, and they go through the golden side door in `skill:eval-loop`. Empty list when the judge believed every key. Written from the same scan that prints the end-of-run warning, because a warning that lives only in console text is one scrollback away from sending a modelling agent at a model that is already right. |
135
- | `mode` / `setName` / `targetVersion` / `serverVersion` / `traceMode` / `callBudget` / `status` | Defined and accepted, **not yet written by any harness** -- kept in the schema for the platform target and the conductor, which need them. |
143
+ | `doubtedGoldens` | The cases whose golden is not believed: `qid`, `gold_status` (`suspect` or `verified_wrong`), `gold_note`, and `declaredBy` (`judge` or `set`). A status the SET declared is a key somebody already settled, not an opinion the judge formed this run, and attributing both to the judge claimed a judgement that never happened. **Read this before diagnose.** These are dataset issues, not model failures, and they go through the golden side door in `skill:eval-loop`. Empty list when no key is in doubt from either source. Written from the same scan that prints the end-of-run warning, because a warning that lives only in console text is one scrollback away from sending a modelling agent at a model that is already right. |
144
+ | `mode` / `setName` / `serverVersion` / `traceMode` / `callBudget` | Defined and accepted, **not yet written by any harness** -- kept in the schema for the platform target and the conductor, which need them. (`targetVersion` and `status` were in this list and are both written now.) |
136
145
 
137
146
  ## Events
138
147
 
@@ -152,13 +161,15 @@ exist in the flat shape, so the flat shape is the contract.) `kind` is one of:
152
161
  | `qid` | string | |
153
162
  | `sample` | int or null | Which repeat. Required even when null. |
154
163
  | `phase` | string | `baseline` / `loop` / `blind_gate` / `canary` / `final`. `phase` lives here, on the attempt, not in run config. |
155
- | `question_sha` | string | Hash of the exact text the answerer saw. |
164
+ | `question_sha` | string | Hash of the exact text the answerer saw, so two runs can be compared on whether they asked the same thing. A case may carry `questionSha`, the seal stamped at conversion, and that wins when present; the ecommerce set writes a 16-char prefix of it, and a set without one left this null. Neither form proves the question was not edited, since both follow the case as it now reads: that comparison is check 6 of `verify_goldens.py`. |
156
165
  | `submitted` | bool | False when there was no final query. Not a wrong answer. |
157
- | `final_query` | string or null | Required to replay. |
166
+ | `final_query` | string or null | Required to replay. A named view is recorded as the Malloy it stands for, `run: <source> -> <view>`, so every consumer sees one shape and the query re-executes. |
167
+ | `final_query_source` | string or null | How `final_query` was chosen: `declared` (the answer printed it), `last_ok` (the last call the server answered) or `last`. `last` is a warning: a trailing sanity probe may be standing in for the answer's own query. |
158
168
  | `servedRevision` | string or null | From the package actually queried. |
159
169
  | `n_get_context` / `n_execute` / `n_execute_errors` | int | |
160
- | `host_tool_uses` | int | Host-side count, including Read and Shell. |
161
- | `reported_calls` | int | MCP calls the answerer claimed. |
170
+ | `host_tool_uses` | int | EVERY tool use the host logged, MCP calls included. It counted only the non-MCP ones until 2026-09-03, which made the under-report check below true of almost every clean attempt. |
171
+ | `mcp_tool_uses` | int or null | The MCP subset: `get_context` plus `execute_query`. Null on a run written before the split. |
172
+ | `reported_calls` | int | MCP calls the answerer claimed. `reported_calls > host_tool_uses` is the under-report floor from `skill:eval-answer`: the answerer cannot have made more calls than the host logged. `validate_run` warns on it, and only for runs that carry `mcp_tool_uses`, because the field meant something narrower before. |
162
173
  | `contaminated` | bool or `"unknown"` | `"unknown"` when no host log exists. Read it with `ledger.is_contaminated`, never for truthiness: runs written before 2026-09-03 carry the strings `"true"`/`"false"`, and `bool("false")` is True. `ledger.event` now rejects those strings on write; `validate_run` grandfathers them on read as a warning. |
163
174
  | `contamination_reasons` | list | Empty when clean. |
164
175
  | `input_tokens` / `output_tokens` / `cache_read_tokens` | int or null | Answerer token usage. Null when the host does not report it. |
@@ -182,6 +193,9 @@ One event per MCP `get_context` or `execute_query` the attempt made.
182
193
  | `traceId` | string or null | `get_context` only; look up in your host's trace store. |
183
194
  | `targets` | string, list or null | **What the answerer asked for**: the search terms it sent to `get_context`. Null for `execute_query`. |
184
195
  | `rankedSummary` | object | Copied at capture from the trace so evidence survives trace eviction: `entityIds`, `ranks`, `resultCount`, and per-target `targets` with within-target ranks. |
196
+ | `retrieval_mode` | string or null | `get_context` only: the `retrieval` field of the response that answered, `semantic` or `lexical`. Null when the server named none, which means no embedding provider. Recorded per call because the semantic path can fall over partway through a run. |
197
+ | `query` | string or null | `execute_query` only: the Malloy the call sent, so the final query can be chosen by which call the server actually answered. |
198
+ | `modelPath` | string or null | `execute_query` only: the model file the call named. A source does not resolve outside the file that declares it, so re-execution needs this. |
185
199
  | `error` | string or null | |
186
200
 
187
201
  Never persist `execute_query` result rows, givens, or credentials.
@@ -208,19 +222,38 @@ The answer judge's verdict for one attempt (protocol in
208
222
  | Field | Type | Notes |
209
223
  |---|---|---|
210
224
  | `verdict` | string or null | `match` / `near_match` / `no_match` / `needs_human`; null when the attempt is not scorable. Only `match` and `no_match` are decisions; see below. |
211
- | `reason` | string | Why, from the judge; for a null verdict, why not scorable (`not_submitted`, `golden_missing`, `golden_ambiguous`, `contaminated`). |
225
+ | `reason` | string | Why, from the judge; for a null verdict, why not scorable: `not_submitted`, `contaminated`, or `golden_<status>` -- `golden_missing`, `golden_provisional`, `golden_invalid`, `golden_ambiguous`, `golden_verified_wrong`. The `golden_*` reasons are decided before the judge is called, so an unestablished key costs nothing to refuse. |
212
226
  | `confidence` | int or null | 1 to 10. Confidence of 5 or lower forces `needs_human`. |
213
227
  | `column_pairing` | object or null | The judge's named gold-to-prediction column correspondence. |
214
228
  | `judge_version` / `rubric_sha` | string | Pins which rubric produced this verdict. |
215
229
  | `golden_revision` | int | From the case at score time. |
216
230
  | `contaminated` | bool or `"unknown"` | Copied from the attempt; true or unknown means `verdict: null`. |
231
+ | `must_not_use_hits` | list or null | Entries of `golden.mustNotUse` a script found in the final query. A hit forces `verdict: no_match`. Decided by `check_must_not_use.py`, never by the judge: it is a question about query text. Only a BARE name vetoes; an entry with a connective (`X as ...`, `X through ...`) objects to a use of X rather than to X, and goes to the judge with the prose entries and a path's bare leaf. |
232
+ | `judge_verdict` | string or null | What the judge said when a `must_not_use_hits` veto overrode it. Null otherwise, so the judge's own agreement rate stays measurable across vetoes. |
217
233
  | `artifactPath` | string | The full judge output under `artifacts/`. |
218
- | `gold_status` | string | `verified` / `verified_benign` / `suspect` / `verified_wrong`. **From the judge**, which scored against the golden as written and reports separately whether it believes it; falls back to the case's standing `golden.status` when the judge does not say. `verified_wrong` excludes the case from run aggregates. `suspect` and `verified_wrong` route to the golden side door as `dataset` issues, never to improve. |
234
+ | `gold_status` | string | `verified` / `verified_benign` / `suspect` / `verified_wrong`, and never anything else: the case file's `golden.status` is a DIFFERENT vocabulary that shares only `verified` and `verified_wrong`, and only those two fall back into this field. A `provisional` / `invalid` / `ambiguous` key is reported through the verdict's `reason` (`golden_<status>`) instead, so nothing downstream has to match on two vocabularies in one field. **From the judge**, which scored against the golden as written and reports separately whether it believes it; falls back to the case's standing `golden.status` when the judge does not say, written onto the verdict itself so the run summary and the aggregates see it too. `verified_wrong` excludes the case from run aggregates -- from the judge OR from the case file; assigning the fallback into a copy of the verdict meant a key the set had marked wrong stayed in the aggregates unless the judge independently agreed. `suspect` and `verified_wrong` route to the golden side door as `dataset` issues, never to improve. |
219
235
  | `gold_note` | string or null | The judge's evidence for a non-`verified` status: the two values, or the model line against the rubric sentence. Null when `verified`. |
220
236
 
221
- A `submitted: false` attempt gets `verdict: null, reason: "not_submitted"`,
222
- except for an `unanswerable` golden, where a refusal that names the gap is the
223
- pass and a confident numeric answer is the fail.
237
+ A `golden_*` reason means the ANSWER KEY is not something a verdict can be
238
+ issued against. A golden the set stamps `provisional`, `invalid` or `ambiguous`
239
+ is refused, as is a case with no golden at all and one the set marks
240
+ `verified_wrong`; a golden with NO `status` is unguarded rather than
241
+ unscorable, because sets that predate the field would otherwise score zero of
242
+ zero. A golden holding no value is not refused either: that is how a
243
+ `kind: unanswerable` case and a `kind: criteria` golden are written. This
244
+ matters most for an imported set, because `skill:eval-import` stamps
245
+ `provisional` on every golden it writes and enforces it -- so a set is
246
+ unscorable by construction until its keys are re-derived, and `run_baseline`
247
+ refuses to spend at all when every case in a set is in that state.
248
+
249
+ `not_submitted` means the attempt produced NOTHING to judge: no answer text and
250
+ no query. An attempt that wrote prose without querying **is judged**, and
251
+ against a golden that holds a value an answer containing none of it is
252
+ `no_match` (`skill:eval-judge`, `reference/refusal.md` rule 10). Excusing those
253
+ as unscorable dropped a confident refusal on an answerable case out of the pass
254
+ rate, which is the one thing the answerable-sounds-unanswerable cases exist to
255
+ measure. A refusal is exempt only where `golden.kind` is `unanswerable`, and
256
+ there naming the gap is the pass and a confident number is the fail.
224
257
 
225
258
  Aggregates count decided verdicts only. `match` and `no_match` are the pass and
226
259
  the fail; **`near_match`, `needs_human` and null are none of the above** and stay
@@ -21,7 +21,7 @@ The log is a JSON object:
21
21
  "toolUses": [
22
22
  {"name": "Read", "input": {"path": "..."}},
23
23
  {"name": "Shell", "input": {"command": "..."}},
24
- {"name": "malloy_getContext", "input": {}}
24
+ {"name": "get_context", "input": {}}
25
25
  ],
26
26
  "reportedCalls": 4
27
27
  }
@@ -148,7 +148,7 @@ def check(
148
148
  name = _tool_name(entry) or f"use[{i}]"
149
149
  hay = _haystack(entry)
150
150
  hits = _matches(hay, needles)
151
- # modelPath on malloy_executeQuery is required, not a file read --
151
+ # modelPath on execute_query is required, not a file read --
152
152
  # including when the MCP call rides over Shell as a curl JSON-RPC.
153
153
  if model_needles and _is_host_file_tool(name) and not _is_mcp_transport(hay):
154
154
  hits.extend(_matches(hay, model_needles))