@malloy-publisher/skills 0.1.20 → 0.1.22
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 +45 -5
- package/skills/eval-answer/reference/definition-ledger.md +305 -0
- package/skills/eval-answer/reference/ledger-schema.md +36 -7
- package/skills/eval-answer/scripts/check_coverage.py +12 -21
- package/skills/eval-answer/scripts/check_coverage_test.py +22 -0
- package/skills/eval-answer/scripts/check_findable.py +386 -0
- package/skills/eval-answer/scripts/check_findable_test.py +326 -0
- package/skills/eval-answer/scripts/json_scan.py +43 -0
- package/skills/eval-answer/scripts/ledger.py +63 -7
- package/skills/eval-answer/scripts/ledger_test.py +7 -0
- package/skills/eval-answer/scripts/mcp_payload.py +111 -2
- package/skills/eval-answer/scripts/mcp_payload_test.py +73 -2
- package/skills/eval-answer/scripts/publisher_rest.py +10 -4
- package/skills/eval-answer/scripts/publisher_rest_test.py +56 -0
- package/skills/eval-answer/scripts/score_retrieval.py +326 -41
- package/skills/eval-answer/scripts/score_retrieval_test.py +537 -15
- package/skills/eval-answer/scripts/verify_definitions.py +752 -0
- package/skills/eval-answer/scripts/verify_definitions_test.py +820 -0
- package/skills/eval-answer/scripts/verify_goldens.py +336 -22
- package/skills/eval-answer/scripts/verify_goldens_test.py +261 -2
- package/skills/eval-diagnose/SKILL.md +47 -2
- package/skills/eval-diagnose/scripts/cluster_failures.py +57 -14
- package/skills/eval-diagnose/scripts/cluster_failures_test.py +127 -0
- package/skills/eval-diagnose/scripts/diagnose.py +471 -42
- package/skills/eval-diagnose/scripts/diagnose_test.py +421 -0
- package/skills/eval-import/SKILL.md +180 -1
- package/skills/eval-import/reference/case-format.md +1 -1
- package/skills/eval-improve/SKILL.md +1 -1
- package/skills/eval-improve/scripts/improve.py +29 -3
- package/skills/eval-improve/scripts/improve_test.py +51 -0
- package/skills/eval-judge/SKILL.md +1 -1
- package/skills/eval-loop/SKILL.md +112 -3
- package/skills/eval-loop/reference/measurement.md +44 -4
- package/skills/eval-loop/reference/running-a-run.md +5 -1
- package/skills/eval-loop/scripts/agent_harness.py +21 -20
- package/skills/eval-loop/scripts/agent_harness_test.py +136 -0
- package/skills/eval-loop/scripts/build_run_package.py +9 -3
- package/skills/eval-loop/scripts/flip_table.py +107 -2
- package/skills/eval-loop/scripts/flip_table_test.py +165 -0
- package/skills/eval-loop/scripts/run_baseline.py +956 -97
- package/skills/eval-loop/scripts/run_baseline_test.py +799 -1
- package/skills/eval-loop/templates/eval-run-package/README.md +19 -5
- package/skills/eval-loop/templates/eval-run-package/eval_run.malloy +48 -19
- package/skills/eval-loop/templates/eval-run-package/eval_run.malloynb +11 -4
- package/skills/eval-loop/templates/eval-run-package/public/app.js +12 -4
- package/skills/eval-report/SKILL.md +362 -0
- package/skills/malloy/SKILL.md +2 -1
- package/skills/malloy-analysis/SKILL.md +1 -1
- package/skills/malloy-analysis-pitfalls/SKILL.md +13 -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 +1 -1
- 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 +43 -24
- package/skills/malloy-gotchas-rendering/SKILL.md +1 -1
- package/skills/malloy-html-data-app-embedding/SKILL.md +1 -1
- package/skills/malloy-html-data-app-runtime/SKILL.md +2 -2
- package/skills/malloy-html-data-apps/SKILL.md +1 -1
- package/skills/malloy-lookml-review/SKILL.md +1 -1
- package/skills/malloy-lookml-review/reference/curate-visibility.md +1 -1
- package/skills/malloy-materialization/SKILL.md +3 -3
- package/skills/malloy-materialization-tuning/SKILL.md +2 -2
- package/skills/malloy-model/SKILL.md +51 -17
- package/skills/malloy-model-as-you-go/SKILL.md +1 -1
- package/skills/malloy-modeling/SKILL.md +1 -1
- package/skills/malloy-notebook-chat/SKILL.md +1 -1
- package/skills/malloy-notebooks/SKILL.md +2 -2
- package/skills/malloy-patterns/SKILL.md +1 -1
- package/skills/malloy-phrase-detection/SKILL.md +3 -2
- package/skills/malloy-publish/SKILL.md +2 -2
- package/skills/malloy-queries/SKILL.md +11 -12
- package/skills/malloy-review/SKILL.md +1 -1
- package/skills/malloy-scope/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.22
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Evaluate One Answer
|
|
@@ -123,11 +123,16 @@ a returned source's documentation -- text the answerer reads and acts on. Only
|
|
|
123
123
|
`missing` is a retrieval miss; the route per entity is recorded so the strict
|
|
124
124
|
count is still there.
|
|
125
125
|
|
|
126
|
-
Recall 1.0 with a wrong answer exonerates retrieval:
|
|
126
|
+
Recall 1.0 with a wrong answer exonerates retrieval: everything arrived. Whether
|
|
127
|
+
the agent misused it or the docs never said how to use it is `eval-diagnose`'s
|
|
128
|
+
call, sufficiency first, so the row reads `delivered, wrong` and names no owner.
|
|
127
129
|
Recall below 1.0 and `coverage: covered` means the entity existed and search did
|
|
128
|
-
not surface it
|
|
129
|
-
|
|
130
|
-
|
|
130
|
+
not surface it -- a documentation finding, because the retrieval algorithm is
|
|
131
|
+
fixed (semantic search over doc strings) and an entity that exists and does not
|
|
132
|
+
come back is one whose docs do not say what people ask; `eval-diagnose` calls it
|
|
133
|
+
`NOT-RETURNED`, owner model. `derivable` or `absent` means there was nothing to
|
|
134
|
+
surface. Those look identical in an answer score and have different owners,
|
|
135
|
+
which is what makes this number worth having. It uses the search terms the answerer chose, so it
|
|
131
136
|
attributes a failure *within* an arm and does not compare retrieval across arms
|
|
132
137
|
-- that is the engine-side `eval-retrieval` skill, which does not ship here.
|
|
133
138
|
|
|
@@ -155,6 +160,13 @@ How to invoke it. Either `--model <file-or-dir>` for a local package or
|
|
|
155
160
|
python3 check_coverage.py --set evals/ecommerce --model model.malloy --version 0.0.58
|
|
156
161
|
```
|
|
157
162
|
|
|
163
|
+
Then hand the report back to the run: `run_baseline.py --coverage <report>`.
|
|
164
|
+
Its per-case verdict beats the case's authored `coverage` label in retrieval
|
|
165
|
+
attribution, `run.json` records which report was read, and each retrieval row
|
|
166
|
+
says whether `measured`, `authored` or `none` charged the failure. Without a
|
|
167
|
+
report, a case with no label is attributed to nobody rather than to the model,
|
|
168
|
+
which is what used to happen.
|
|
169
|
+
|
|
158
170
|
Two flags change what the number means, so choose them rather than inheriting
|
|
159
171
|
them. `--repeat N` samples each case N times and takes the majority; it defaults
|
|
160
172
|
to **1 for a set**, because the score is a trend over many cases rather than a
|
|
@@ -189,6 +201,34 @@ stops being stable. Do not take the over-size message's advice to narrow
|
|
|
189
201
|
`--model` to one file, which drops every imported source and manufactures
|
|
190
202
|
`COVERAGE` verdicts.
|
|
191
203
|
|
|
204
|
+
## Validate the definitions, not every answer
|
|
205
|
+
|
|
206
|
+
A golden must not be derived through the definitions the question TESTS. It may
|
|
207
|
+
reuse everything the question does not turn on -- joins, base sources, date
|
|
208
|
+
handling -- because circularity only bites where the key and the answer share the
|
|
209
|
+
step under measurement. That is what makes this affordable on a model too large
|
|
210
|
+
to reimplement.
|
|
211
|
+
|
|
212
|
+
`scripts/verify_definitions.py` checks each definition once against the layer
|
|
213
|
+
directly beneath it, and every case depending on it inherits the result. A golden
|
|
214
|
+
is trustworthy if it was derived independently, OR if every definition it tests
|
|
215
|
+
has itself been validated.
|
|
216
|
+
|
|
217
|
+
Two things it will not do, and both matter more than what it does. It never
|
|
218
|
+
reports a definition reaching through a join as validated, because fanout
|
|
219
|
+
inflates the measure and the control expression equally and the comparison stays
|
|
220
|
+
green on a broken join. And building the ledger without a server exits 3, not 0:
|
|
221
|
+
nothing was checked, and a caller must not read that as a pass.
|
|
222
|
+
|
|
223
|
+
A raw check -- a definition that reaches through a join -- is an authored
|
|
224
|
+
control: a person writes the population down as a query on the ledger record and
|
|
225
|
+
the tool re-runs it every time. It is not found unaided, and the docs say so.
|
|
226
|
+
`verify_goldens.py --definitions <ledger>` then applies the composition rule at
|
|
227
|
+
the gate: a set with no truth package exits 0 when every value-bearing case's
|
|
228
|
+
tested definitions are validated, and names the cases that are not.
|
|
229
|
+
|
|
230
|
+
**Read `reference/definition-ledger.md` before building or quoting one.**
|
|
231
|
+
|
|
192
232
|
## Step 5: Distrust the golden
|
|
193
233
|
|
|
194
234
|
A reference answer can be wrong (parent-column fanout, a join on a shared
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
<!-- Validating a model's definitions once, so answer keys need not be re-derived per case. Read before building or reading a ledger. -->
|
|
2
|
+
|
|
3
|
+
# The definition ledger
|
|
4
|
+
|
|
5
|
+
## Why it exists
|
|
6
|
+
|
|
7
|
+
The loop's standing rule is that a golden must never be derived through the model
|
|
8
|
+
under test, because a model bug would otherwise certify its own golden. That rule
|
|
9
|
+
is sound, and it does not scale: on a model with real transformation, deriving
|
|
10
|
+
every key independently means reimplementing the model. You end up with two
|
|
11
|
+
implementations and no reason to trust either, and the eval task becomes *rebuild
|
|
12
|
+
the model to verify the model*.
|
|
13
|
+
|
|
14
|
+
The ledger is the other half. **A definition is checked once, against the layer
|
|
15
|
+
directly beneath it, and every case depending on it inherits the result.** Work
|
|
16
|
+
scales with the number of definitions rather than the number of cases, and stops
|
|
17
|
+
growing as cases are added.
|
|
18
|
+
|
|
19
|
+
The rule this replaces:
|
|
20
|
+
|
|
21
|
+
> A key must be independent of **the definitions the question tests**, not of the
|
|
22
|
+
> whole model.
|
|
23
|
+
|
|
24
|
+
And the rule that makes that sound:
|
|
25
|
+
|
|
26
|
+
> A golden is trustworthy if it was derived independently, **or** if every
|
|
27
|
+
> definition it tests has itself been validated.
|
|
28
|
+
|
|
29
|
+
## Deep models are cheaper per case, not dearer
|
|
30
|
+
|
|
31
|
+
The tractability argument is about definitions being **shared**, not about models
|
|
32
|
+
being shallow. An earlier draft claimed every measure sits one or two hops from a
|
|
33
|
+
raw column; measured across four models, that is false:
|
|
34
|
+
|
|
35
|
+
| Model | Definitions | within 2 hops | deepest chain |
|
|
36
|
+
|---|---|---|---|
|
|
37
|
+
| ecommerce (sample) | 32 | 90.6% | 3 |
|
|
38
|
+
| storefront (sample) | 19 | 94.7% | 3 |
|
|
39
|
+
| a 492-definition benchmark | 492 | 100% | 2 |
|
|
40
|
+
| a real 548-definition customer model | 548 | **42.5%** | **12** |
|
|
41
|
+
|
|
42
|
+
On that customer model, the twenty deepest measures close over **812**
|
|
43
|
+
definition-checks with no reuse and **116** with the ledger: an 86% saving.
|
|
44
|
+
Reuse pays off *more* as chains deepen, because deep chains share their lower
|
|
45
|
+
layers. Depth is what makes the ledger worth having.
|
|
46
|
+
|
|
47
|
+
## What a record holds
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"entityId": "measure:order_items:total_sales",
|
|
52
|
+
"kind": "measure",
|
|
53
|
+
"source": "order_items",
|
|
54
|
+
"name": "total_sales",
|
|
55
|
+
"expr": "sale_price.sum()",
|
|
56
|
+
"exprSha": "3f9a…",
|
|
57
|
+
"depends": ["measure:order_items:order_item_count"],
|
|
58
|
+
"check": {"kind": "within_model", "slice": "first 200 rows"},
|
|
59
|
+
"verdict": "agrees",
|
|
60
|
+
"needs": null
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`exprSha` is a **Merkle chain**, not a hash of the one line: it covers the
|
|
65
|
+
definition and everything beneath it. A measure whose own text never moved is
|
|
66
|
+
still stale once something it builds on changes, and a flat hash would report it
|
|
67
|
+
as current. `packageSha` exists already but is whole-package, so it invalidates
|
|
68
|
+
everything on any edit and distinguishes nothing.
|
|
69
|
+
|
|
70
|
+
## The two check kinds
|
|
71
|
+
|
|
72
|
+
### `within_model` -- no raw access, no truth package
|
|
73
|
+
|
|
74
|
+
Ask the model for the entity and for the expression it **claims** to be, in one
|
|
75
|
+
query, and compare:
|
|
76
|
+
|
|
77
|
+
```malloy
|
|
78
|
+
run: order_items -> {
|
|
79
|
+
aggregate:
|
|
80
|
+
stated is total_sales
|
|
81
|
+
control is sale_price.sum()
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Both sides come from **one** query against one source, so they see identical
|
|
86
|
+
rows: a difference is the definition, not a moving population.
|
|
87
|
+
|
|
88
|
+
A dimension is scalar, and `aggregate:` rejects it outright ("Cannot use a scalar
|
|
89
|
+
field in an aggregate"), so a dimension is compared row by row over a slice. The
|
|
90
|
+
slice is recorded, because a slice is weaker evidence than a whole table and the
|
|
91
|
+
report must not imply otherwise.
|
|
92
|
+
|
|
93
|
+
This catches a definition that is not what it says: a filter nobody mentioned, a
|
|
94
|
+
renamed column, the wrong aggregate, or a served model that has moved away from
|
|
95
|
+
the text the ledger recorded.
|
|
96
|
+
|
|
97
|
+
### `raw` -- needs a lens onto the base tables
|
|
98
|
+
|
|
99
|
+
**A within-model check is blind to fanout.** A join that duplicates rows inflates
|
|
100
|
+
the stated measure and the control expression equally, and the comparison stays
|
|
101
|
+
green on a genuinely broken join. So an expression reaching through a join records
|
|
102
|
+
`unchecked` with a reason, **never `agrees`** -- reporting it as validated would be
|
|
103
|
+
the tool committing the exact error it exists to find.
|
|
104
|
+
|
|
105
|
+
The exception is aggregates that survive uniform duplication: `AVG`, `STDDEV`,
|
|
106
|
+
`MIN`, `MAX`. Those stay checkable across a join.
|
|
107
|
+
|
|
108
|
+
Detection is by join **name**, not by spotting a dot. `sale_price.sum()` is a
|
|
109
|
+
method call on a column in the same source and is perfectly checkable;
|
|
110
|
+
`inventory_items.cost.sum()` traverses a join and is not. A first implementation
|
|
111
|
+
matched any `word.word` and held back `total_sales` -- the measure twelve of the
|
|
112
|
+
ecommerce set's cases depend on -- for a join it does not cross.
|
|
113
|
+
|
|
114
|
+
## A raw check is a control a person wrote
|
|
115
|
+
|
|
116
|
+
An earlier version of this page, and the plan behind it, said raw checks would
|
|
117
|
+
"catch a definition wrong about the business, like the cogs case." **That is
|
|
118
|
+
retracted.** Re-deriving `total_gross_margin`'s own expression over the base
|
|
119
|
+
tables -- reconstructing the model's join from its declared key -- computes the
|
|
120
|
+
same quantity, cancelled lines included, and agrees. Mechanically that catches
|
|
121
|
+
only fanout from a mis-declared join, which Malloy's symmetric aggregates already
|
|
122
|
+
mostly prevent. It cannot see that the population is wrong.
|
|
123
|
+
|
|
124
|
+
What caught the cogs case was a person who read the `status` doc ("Cancelled
|
|
125
|
+
lines never ship and are not sales") and wrote `where: status != 'Cancelled'`
|
|
126
|
+
into the golden's query. That is an **authored assertion**. So a raw check is
|
|
127
|
+
exactly that, made durable: the conductor writes a control query onto the ledger
|
|
128
|
+
record, and `verify_definitions.py` re-runs and compares it on every rebuild.
|
|
129
|
+
|
|
130
|
+
```json
|
|
131
|
+
{
|
|
132
|
+
"entityId": "measure:order_items:total_gross_margin",
|
|
133
|
+
"check": {
|
|
134
|
+
"kind": "raw",
|
|
135
|
+
"against": "model",
|
|
136
|
+
"query": "run: order_items -> { aggregate: control is sale_price.sum() { where: status != 'Cancelled' } - inventory_items.cost.sum() { where: status != 'Cancelled' } }",
|
|
137
|
+
"note": "excludes Cancelled, per status's own doc and units_sold"
|
|
138
|
+
},
|
|
139
|
+
"cause": "CONVENTION"
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
A control may be authored on **any** measure record, not only a `raw` one, and
|
|
144
|
+
it supersedes the within-model check. That check can only say a measure is what
|
|
145
|
+
its expression says; a person can say what its population should be. `total_sales
|
|
146
|
+
is sale_price.sum()` agrees with itself while including the cancelled lines its
|
|
147
|
+
own docs exclude -- the within-model check reads `agrees`, and it is the measure
|
|
148
|
+
twelve of the sample set's cases depend on. A one-line control on it is the
|
|
149
|
+
whole difference.
|
|
150
|
+
|
|
151
|
+
The control returns one row with a `control` column. `against: model` runs it on
|
|
152
|
+
the same server as the measure and is the default -- it reuses the model's joins
|
|
153
|
+
and columns and asserts only the population. `against: truth` runs it on a truth
|
|
154
|
+
package of raw tables (`--truth-publisher`, `--truth-package`) for a fully
|
|
155
|
+
independent derivation, join included. The measure's own value is always fetched
|
|
156
|
+
through the model, separately, because the control is *supposed* to differ in
|
|
157
|
+
population and may run elsewhere.
|
|
158
|
+
|
|
159
|
+
Authoring a control is judgement, and the same rules as authoring a golden apply:
|
|
160
|
+
where two populations are both defensible from the docs, do not pick one --
|
|
161
|
+
record the ambiguity in `note` and leave the record `unchecked`. `cause` is the
|
|
162
|
+
`eval-diagnose` code the author expects a disagreement to mean (the cogs case is
|
|
163
|
+
`CONVENTION`); it is carried onto the finding, not inferred.
|
|
164
|
+
|
|
165
|
+
This is **validate what someone asserted**, not **find business-wrong definitions
|
|
166
|
+
unaided**. It turns "I believe `total_gross_margin` should exclude cancelled
|
|
167
|
+
lines" into a checked, re-runnable assertion with a sha, so the belief is tested
|
|
168
|
+
on every rebuild instead of once in someone's head. The difference is the whole
|
|
169
|
+
point, and the run report must not blur it.
|
|
170
|
+
|
|
171
|
+
### Build once, then maintain
|
|
172
|
+
|
|
173
|
+
`--out` writes a fresh ledger. `--ledger <file>` reads an existing one, keeps
|
|
174
|
+
every field a person wrote (`check.query`, `check.against`, `check.note`,
|
|
175
|
+
`cause`), re-parses the model, re-runs every check, and writes it back. Nothing
|
|
176
|
+
else is carried: a verdict is never inherited from a definition that may have
|
|
177
|
+
moved, and a record whose definition the model no longer declares is dropped,
|
|
178
|
+
control and all -- a control for a measure that does not exist is not a
|
|
179
|
+
validated measure.
|
|
180
|
+
|
|
181
|
+
## What it will not claim
|
|
182
|
+
|
|
183
|
+
Every one of these records `unchecked`, and a reader must treat `unchecked` as
|
|
184
|
+
"nobody has established this", never as "fine":
|
|
185
|
+
|
|
186
|
+
| Situation | Why not checked |
|
|
187
|
+
|---|---|
|
|
188
|
+
| reaches through a join, and no control has been authored | fanout moves both sides of a within-model check equally, and nobody has written down what the population should be |
|
|
189
|
+
| spans more than one line | only the first line was read, so the recorded expression is a fragment |
|
|
190
|
+
| the query failed | a check that could not run is not a check that passed |
|
|
191
|
+
| no rows came back | nothing to compare |
|
|
192
|
+
| every sampled row null on both sides | proves nothing about the definition |
|
|
193
|
+
| no enclosing source | nothing to query it against |
|
|
194
|
+
|
|
195
|
+
## Exit codes
|
|
196
|
+
|
|
197
|
+
Matching `verify_goldens.py`, deliberately:
|
|
198
|
+
|
|
199
|
+
| | |
|
|
200
|
+
|---|---|
|
|
201
|
+
| **0** | every check that could run ran, and agreed |
|
|
202
|
+
| **1** | a definition disagrees with its own stated expression |
|
|
203
|
+
| **3** | could not run: no model, no server, or a ledger built and never checked |
|
|
204
|
+
|
|
205
|
+
**3 is load-bearing.** Building the ledger without `--publisher` returns 3, not 0:
|
|
206
|
+
it validated nothing, and a caller must never read that as a pass.
|
|
207
|
+
|
|
208
|
+
## Running it
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
# build the ledger only (exits 3: nothing was checked)
|
|
212
|
+
python3 verify_definitions.py --model ecommerce.malloy --out evals/definitions/ecommerce.jsonl
|
|
213
|
+
|
|
214
|
+
# build and check
|
|
215
|
+
python3 verify_definitions.py --model ecommerce.malloy \
|
|
216
|
+
--publisher http://localhost:4811 --environment samples \
|
|
217
|
+
--package ecommerce --model-path ecommerce.malloy \
|
|
218
|
+
--out evals/definitions/ecommerce.jsonl
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Measured on the ecommerce sample: **28 definitions, 15 agree, 13 unchecked**, of
|
|
222
|
+
which 4 need a raw check. The remainder are multi-line definitions and fields
|
|
223
|
+
declared inside a nested block, which the line scanner attributes to the
|
|
224
|
+
enclosing source and cannot address.
|
|
225
|
+
|
|
226
|
+
## What a run reports
|
|
227
|
+
|
|
228
|
+
`run_baseline.py --definitions <ledger>` adds an EVIDENCE block saying what the
|
|
229
|
+
pass rate rests on, per case:
|
|
230
|
+
|
|
231
|
+
```
|
|
232
|
+
EVIDENCE
|
|
233
|
+
basis 5 independent, 8 definitions, 36 unchecked
|
|
234
|
+
! those cases rest on a definition nobody has validated. Not a
|
|
235
|
+
failure, and not a pass either.
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
| basis | meaning |
|
|
239
|
+
|---|---|
|
|
240
|
+
| `independent` | the key was established outside the model: two differently shaped derivations agree, or it holds no value at all (`criteria`, `unanswerable`) |
|
|
241
|
+
| `definitions` | the key went through the model, and every definition it tests is validated |
|
|
242
|
+
| `unchecked` | at least one tested definition is unvalidated, stale, or absent from the ledger |
|
|
243
|
+
| `disagrees` | a definition the case depends on contradicts its own expression, so the model is wrong before the answer is |
|
|
244
|
+
|
|
245
|
+
Independence is read from `golden.verification` (or `gold/<qid>.json`) through
|
|
246
|
+
`verify_goldens.has_second_derivation` -- the structured record of "two
|
|
247
|
+
differently shaped derivations agree". **Not from `verifiedBy`,** which is free
|
|
248
|
+
text: a first implementation matched it by prefix and classified 34 goldens of
|
|
249
|
+
the ecommerce set as `unchecked` when every one of them reads "authored and
|
|
250
|
+
re-derived against ecommerce-truth". A well-founded set reading as unvalidated
|
|
251
|
+
is the same over-claim as an unfounded one reading as validated, pointed the
|
|
252
|
+
other way.
|
|
253
|
+
|
|
254
|
+
Which definitions a case tests comes from `expectedEntities`, which already
|
|
255
|
+
names entities in the `kind:source:name` form the ledger keys on. No new
|
|
256
|
+
hand-maintained field: a wrong entity id is the failure mode that cost a real
|
|
257
|
+
set two days, so this reuses a link the set already maintains and that
|
|
258
|
+
`verify_goldens` check 5 audits against the model.
|
|
259
|
+
|
|
260
|
+
**Without `--definitions` there is no EVIDENCE block at all.** Absent is a
|
|
261
|
+
different fact from checked, and a reassuring empty block would be the
|
|
262
|
+
over-claim this whole direction exists to remove.
|
|
263
|
+
|
|
264
|
+
Staleness is a hash comparison against the run's own `model.malloy` snapshot --
|
|
265
|
+
the bytes that actually answered -- so it costs nothing and runs every arm.
|
|
266
|
+
|
|
267
|
+
## The composition rule at the gate
|
|
268
|
+
|
|
269
|
+
`verify_goldens.py` used to exit 3 -- "could not run" -- on any set that named no
|
|
270
|
+
truth package, because no golden value could be re-derived. That was right when
|
|
271
|
+
re-derivation was the only evidence there was. With a ledger it is one of two:
|
|
272
|
+
|
|
273
|
+
> A golden is trustworthy if it was derived independently, **or** if every
|
|
274
|
+
> definition it tests has itself been validated.
|
|
275
|
+
|
|
276
|
+
`verify_goldens.py --definitions <ledger>` applies the second half. For each
|
|
277
|
+
value-bearing case it takes the entities the case names in `expectedEntities` --
|
|
278
|
+
the same ids the ledger is keyed on -- and asks whether every one is `agrees` in
|
|
279
|
+
the ledger and not stale against `--model`. If every case passes, the absent truth
|
|
280
|
+
server is not a check that failed to happen, and the audit exits 0 with a line
|
|
281
|
+
saying what validated it; `--promote` may then promote a `provisional` golden that
|
|
282
|
+
also carries a second derivation, and `verifiedBy` names the ledger. If any case
|
|
283
|
+
does not pass, the exit stays 3 and the message names the cases and the
|
|
284
|
+
definitions that let them down. Nothing is inferred about a case that names no
|
|
285
|
+
entities: it is unvalidated, and it says so.
|
|
286
|
+
|
|
287
|
+
`improve.py` passes `--definitions` through when it has one, so the acceptance
|
|
288
|
+
check can pass on a set that never had a truth package.
|
|
289
|
+
|
|
290
|
+
## Where it goes
|
|
291
|
+
|
|
292
|
+
`evals/definitions/<package>.jsonl` -- keyed by **package, not by set**, because
|
|
293
|
+
definitions belong to the model and outlive any one set, and several sets against
|
|
294
|
+
the same package share them. Never under the served package tree; the golden side
|
|
295
|
+
door forbids eval artifacts there.
|
|
296
|
+
|
|
297
|
+
## What this is not
|
|
298
|
+
|
|
299
|
+
It is not a unit test. Publisher's roadmap plans fixture-based unit tests (a CSV
|
|
300
|
+
per source plus an expected result) alongside data tests in `malloy-pub lint`.
|
|
301
|
+
A fixture unit test proves an expression computes what you wrote -- so it would
|
|
302
|
+
**not** catch a measure whose expression is correct and whose *population* is
|
|
303
|
+
wrong, which is the defect class this exists for. The ledger is a data test, and
|
|
304
|
+
it should hand off to `lint` when that governance work lands rather than becoming
|
|
305
|
+
a permanent parallel mechanism.
|
|
@@ -87,7 +87,7 @@ One JSON object per line:
|
|
|
87
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. |
|
|
88
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. |
|
|
89
89
|
| `goldenRevision` | Integer. Bump on every golden change; `score` events stamp the revision they compared against. |
|
|
90
|
-
| `expectedEntities` | `required`: entity ids (`kind:source:name`) the answer cannot be produced without. `requiredAnyOf`: a list of **groups**, each a list of ids of which any one suffices -- for a case the model can answer through more than one route. Naming only one route scores the other as a retrieval miss and steers
|
|
90
|
+
| `expectedEntities` | `required`: entity ids (`kind:source:name`) the answer cannot be produced without. `requiredAnyOf`: a list of **groups**, each a list of ids of which any one suffices -- for a case the model can answer through more than one route. Naming only one route scores the other as a retrieval miss and steers the fix to the docs for an entity whose docs were never the problem. `acceptable`: ids that are not noise if returned. An entity counts as delivered when returned under its exact id, under the same type and name on a sibling source, or by name in a returned source's documentation; `score_retrieval.py` records the route per entity as `delivery`. |
|
|
91
91
|
|
|
92
92
|
## Term files (`eval-retrieval` only)
|
|
93
93
|
|
|
@@ -116,6 +116,7 @@ on that axis.
|
|
|
116
116
|
| `answererModel` *(required)* | |
|
|
117
117
|
| `phase` / `started` *(required)* | |
|
|
118
118
|
| `judgeModel` / `judgeVersion` / `rubricSha` *(pins)* | The judge's model, and the version + content sha of `skill:eval-judge`. |
|
|
119
|
+
| `judgePromptSha` *(pin)* | Content sha of the prompt the judge is SHOWN, which is the harness's own template and not the skill file `rubricSha` covers. The template carries the golden rendering, the evidence blocks and the tie-breaking rules, so two runs whose prompts differed compared as the same judge -- the blind spot that let a golden-rendering defect run through three arms with every test passing, because the tests covered the code paths and not the text the model sees. Absent on runs written before it existed. |
|
|
119
120
|
| `datasetVersion` *(pin)* | From `set.json` at run time. |
|
|
120
121
|
| `modelSha` *(pin)* | Content sha of the `model.malloy` snapshot in the run directory -- the bytes the answerer actually queried. A git sha is not enough: a snapshot host serves a copy, often of a dirty tree no commit names. |
|
|
121
122
|
| `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. |
|
|
@@ -132,12 +133,16 @@ on that axis.
|
|
|
132
133
|
| `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
134
|
| `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
135
|
| `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. |
|
|
136
|
+
| `coverageReport` | The `check_coverage.py --out` report this run read, when `--coverage` named one: its path, the model version it was stamped with, its `agentModel`, and `decided` of `cases`. Coverage is a read of the MODEL, so it is measured separately and costs a judge call per case; the run does not run it, it consumes it. Its per-case verdict then beats the authored `coverage` label in retrieval attribution, and each `score_retrieval` row records `coverage_source` (`measured`, `authored`, `none`) so nobody has to guess which one charged the failure. Absent when no report was given, which is a different fact from a report that found everything covered. |
|
|
135
137
|
| `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. |
|
|
136
138
|
| `effort` | |
|
|
137
|
-
| `
|
|
139
|
+
| `maxTurns` / `answererTimeout` *(pins)* | The answerer's turn cap and per-attempt timeout, as the arm ran them. `skill:eval-loop` step 6 lists the call budget among the pins to freeze for a whole run, and nothing wrote one, so no two runs could be compared on the setting that decides whether a case got to finish at all. Distinct from `callBudget`, which no harness has ever written. Size the cap from a pilot rather than taking the default: on a 29-case arm at the default 30, completed attempts had a median of 17 turns and a 90th percentile of 26, and the four cases that ran past it were recorded as wrong answers. |
|
|
140
|
+
| `truncated` / `contaminated` | The qids excluded for a HARNESS-owned reason: the cap cut the answer off, or the attempt breached isolation. Both leave the denominator, both are named here rather than only in the console, and either one makes `status` `incomplete`. |
|
|
141
|
+
| `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. Per-case judge cost is on each `score` event. |
|
|
142
|
+
| `answererCostCopiedFrom` | The run whose attempts `answererCostUsd` was COPIED from, when this run spawned no answerer -- a `--rejudge`, a `--rebuild`, a `--from`. Those copy the source's attempt events verbatim, `cost_usd` included, so the figure is right about the attempts and wrong as this run's spend: one re-judge reported $17.56 of answering that never happened. Null when this run paid, so a total over several run files can skip the copies. |
|
|
138
143
|
| `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
144
|
| `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 `[]`. |
|
|
140
|
-
| `status` | `complete
|
|
145
|
+
| `status` | `complete`; `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; or `incomplete` when the arm ran to the end and still cannot report a rate, because `truncated` or `contaminated` is non-empty. `incomplete` is a third state, not a soft `complete`: the arm did everything asked of it and the number is still unearned. It does NOT stop the run being compared: `flip_table.py` scores the cases both arms did, and names the ones this arm left unscored -- an excluded case carries no verdict, so it can never be counted as a flip. |
|
|
141
146
|
| `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. |
|
|
142
147
|
| `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. |
|
|
143
148
|
| `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. |
|
|
@@ -172,10 +177,11 @@ exist in the flat shape, so the flat shape is the contract.) `kind` is one of:
|
|
|
172
177
|
| `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. |
|
|
173
178
|
| `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. |
|
|
174
179
|
| `contamination_reasons` | list | Empty when clean. |
|
|
175
|
-
| `input_tokens` / `output_tokens` / `cache_read_tokens` | int or null | Answerer token usage. Null when the host does not report it. |
|
|
180
|
+
| `input_tokens` / `output_tokens` / `cache_read_tokens` / `cache_write_tokens` | int or null | Answerer token usage, the four counts that reprice a run from its own ledger. Null when the host does not report it. `cost_usd` beside them comes from the CLI's `total_cost_usd`, which prices cache reads and writes at their rates, so it was always cache-inclusive; what was missing was the breakdown that reproduces it. `cache_write_tokens` (`cache_creation_input_tokens` upstream) was never captured before 2026-09-15, and on one analysed run cache writes were 44% of the agent's cost, the single largest line. **Read `input_tokens` with care:** it counts only the tokens after the last cache breakpoint. One run recorded 224 for 24 questions beside 2.4M cache reads; the 224 is not the context volume, and a token total that omits the cache columns is not a size. |
|
|
176
181
|
| `cost_usd` | float or null | Answerer cost for this attempt. |
|
|
177
182
|
| `num_turns` / `wall_seconds` | int, float or null | |
|
|
178
183
|
| `answer_text` | string or null | The answer the judge scored. Kept so a verdict can be re-read without the transcript. |
|
|
184
|
+
| `skills_invoked` | list of string | The skills the answerer invoked through the `Skill` tool, deduplicated and sorted, counted from its own transcript. Always a list on a run that recorded it, so an EMPTY list is the finding: the arm granted a manifest and the agent reached for none of it. Absent on runs written before 2026-09-15, which is why every reader takes `or []`. Only explicit `Skill` calls are counted, and only skills the arm granted -- one outside `--answerer-skills` is a contamination breach and lands in `contamination_reasons` instead. What it is for: a manifest names what an arm GRANTED, and comparing two arms' doctrine is meaningless if the doctrine never loaded. |
|
|
179
185
|
| `transcriptPath` | string | The answerer's transcript under `artifacts/`. |
|
|
180
186
|
|
|
181
187
|
Token counts sit here rather than being derived later because the claim a
|
|
@@ -192,7 +198,8 @@ One event per MCP `get_context` or `execute_query` the attempt made.
|
|
|
192
198
|
| `tool` | string | `get_context` or `execute_query`. |
|
|
193
199
|
| `traceId` | string or null | `get_context` only; look up in your host's trace store. |
|
|
194
200
|
| `targets` | string, list or null | **What the answerer asked for**: the search terms it sent to `get_context`. Null for `execute_query`. |
|
|
195
|
-
| `rankedSummary` | object | Copied at capture
|
|
201
|
+
| `rankedSummary` | object | Copied at capture so evidence survives trace eviction: `entityIds` (identity, in response order), `hits` (one row per entity: `entity_id`, `source`, `relevance`, `matched_targets`, `position`, `position_in_source`), `resultCount`, `docTokens`. **There is no rank.** This field previously documented `ranks` and "per-target `targets` with within-target ranks"; no writer ever wrote either, and what `ranks` did hold was `range(1, n+1)` over a list already flattened source-card by source-card -- a position, not a rank, and not even a relevance order, since the response sorts globally and then buckets into cards. Ask relevance questions of `hits[].relevance`. |
|
|
202
|
+
| `rankedSummary.hits[].matched_targets` | list or null | Which of the caller's search targets matched this entity, each with the server's own relevance. **`null` means no attribution was published**, which is not the same as `[]`: the server attaches it to entities only (a source card's attribution is discarded before an entity object exists) and only on the semantic path, because a lexical score is relative to its own query and comparing two of them means nothing. A lexical run therefore cannot say which target found what, and a reader must report that as unavailable rather than as "never asked". |
|
|
196
203
|
| `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
204
|
| `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
205
|
| `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. |
|
|
@@ -222,14 +229,14 @@ The answer judge's verdict for one attempt (protocol in
|
|
|
222
229
|
| Field | Type | Notes |
|
|
223
230
|
|---|---|---|
|
|
224
231
|
| `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. |
|
|
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
|
|
232
|
+
| `reason` | string | Why, from the judge; for a null verdict, why not scorable, and it is ALWAYS one of these: `not_submitted`, `answerer_truncated`, `contaminated`, `judge_unparseable`, `no_saved_verdict`, `judge_error: <text>`, or `golden_<status>` -- `golden_missing`, `golden_provisional`, `golden_invalid`, `golden_ambiguous`, `golden_verified_wrong`. A null verdict never arrives without one: a nulling that does not say which of these it was is indistinguishable from the field being dropped on the write path, and was read as exactly that from a real run. The `golden_*` reasons and `answerer_truncated` are decided before the judge is called, so neither an unestablished key nor a cut-off attempt costs a judge call. |
|
|
226
233
|
| `confidence` | int or null | 1 to 10. Confidence of 5 or lower forces `needs_human`. |
|
|
227
234
|
| `column_pairing` | object or null | The judge's named gold-to-prediction column correspondence. |
|
|
228
235
|
| `judge_version` / `rubric_sha` | string | Pins which rubric produced this verdict. |
|
|
229
236
|
| `golden_revision` | int | From the case at score time. |
|
|
230
237
|
| `contaminated` | bool or `"unknown"` | Copied from the attempt; true or unknown means `verdict: null`. |
|
|
231
238
|
| `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
|
|
239
|
+
| `judge_verdict` | string or null | What the judge said when a MECHANICAL override replaced it: a `must_not_use_hits` veto, or contamination voiding the verdict. Null otherwise, so the judge's own agreement rate stays measurable across both. When a case is vetoed AND contaminated this still holds the judge's read, not the veto's `no_match`. |
|
|
233
240
|
| `artifactPath` | string | The full judge output under `artifacts/`. |
|
|
234
241
|
| `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. |
|
|
235
242
|
| `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`. |
|
|
@@ -255,6 +262,28 @@ rate, which is the one thing the answerable-sounds-unanswerable cases exist to
|
|
|
255
262
|
measure. A refusal is exempt only where `golden.kind` is `unanswerable`, and
|
|
256
263
|
there naming the gap is the pass and a confident number is the fail.
|
|
257
264
|
|
|
265
|
+
`answerer_truncated` means the answerer hit the run's `maxTurns` cap and the
|
|
266
|
+
CLI returned whatever it had, which is usually a sentence stopping mid-thought.
|
|
267
|
+
It is refused BEFORE the golden gate and before a saved verdict is reused, so
|
|
268
|
+
`--rebuild` over a run made without this re-derives the right ledger from its
|
|
269
|
+
transcripts. A cap is a harness setting, so a case cut off by one says nothing
|
|
270
|
+
about whether the model could answer it: measured on one 29-case arm, four
|
|
271
|
+
attempts ended at exactly 31 turns and each was judged as a wrong answer, one
|
|
272
|
+
of them on the text "Let me build the correct monthly trend query."
|
|
273
|
+
|
|
274
|
+
`contaminated` means the attempt breached isolation (`contamination_reasons`
|
|
275
|
+
names how) and the verdict is withheld rather than lost. The judge's own read
|
|
276
|
+
moves to `judge_verdict`. Before this reason was written, a voided verdict kept
|
|
277
|
+
the judge's prose in `reason` beside `verdict: null`, and a run report
|
|
278
|
+
reasonably concluded the ledger was dropping the field -- it recovered nine
|
|
279
|
+
"lost" verdicts by re-parsing the stored judge replies, every one of which had
|
|
280
|
+
been voided on purpose.
|
|
281
|
+
|
|
282
|
+
Both are HARNESS-owned, not model-owned, and a run containing either reports
|
|
283
|
+
`status: incomplete` in `run.json` and **prints no pass rate**. `unscorable`
|
|
284
|
+
(no established golden) and `judge_unparseable` do not suppress the rate: the
|
|
285
|
+
first is a dataset state no arm can fix, and the second already has a retry.
|
|
286
|
+
|
|
258
287
|
Aggregates count decided verdicts only. `match` and `no_match` are the pass and
|
|
259
288
|
the fail; **`near_match`, `needs_human` and null are none of the above** and stay
|
|
260
289
|
out of acceptance arithmetic. `near_match` is excluded because it means "defensibly
|
|
@@ -43,6 +43,7 @@ from __future__ import annotations
|
|
|
43
43
|
|
|
44
44
|
import argparse
|
|
45
45
|
import concurrent.futures as futures
|
|
46
|
+
import hashlib
|
|
46
47
|
import json
|
|
47
48
|
import pathlib
|
|
48
49
|
import re
|
|
@@ -56,6 +57,7 @@ HERE = pathlib.Path(__file__).resolve().parent
|
|
|
56
57
|
SKILLS_ROOT = HERE.parent.parent
|
|
57
58
|
sys.path.insert(0, str(HERE))
|
|
58
59
|
|
|
60
|
+
from json_scan import json_objects # noqa: E402
|
|
59
61
|
from verify_goldens import model_text as local_model_text # noqa: E402
|
|
60
62
|
|
|
61
63
|
# The `claude -p` invocation lives once, in the harness, rather than being
|
|
@@ -234,27 +236,6 @@ one candidate and its `ruled_out` entry is null, the verdict is not `ok`.
|
|
|
234
236
|
"""
|
|
235
237
|
|
|
236
238
|
|
|
237
|
-
def json_objects(text: str) -> list[dict[str, Any]]:
|
|
238
|
-
"""Every JSON object in `text`, in the order they appear.
|
|
239
|
-
|
|
240
|
-
Decoded from each `{` rather than matched with `\\{.*\\}`, which is greedy
|
|
241
|
-
and spans the FIRST brace to the LAST. The prompt hands the agent the model
|
|
242
|
-
text, so one `extend { ... }` quoted back in the narration would swallow the
|
|
243
|
-
verdict and a good reply would read as unparseable.
|
|
244
|
-
"""
|
|
245
|
-
dec, out, i = json.JSONDecoder(), [], 0
|
|
246
|
-
while (i := text.find("{", i)) >= 0:
|
|
247
|
-
try:
|
|
248
|
-
v, end = dec.raw_decode(text, i)
|
|
249
|
-
except json.JSONDecodeError:
|
|
250
|
-
i += 1
|
|
251
|
-
continue
|
|
252
|
-
if isinstance(v, dict):
|
|
253
|
-
out.append(v)
|
|
254
|
-
i = max(end, i + 1)
|
|
255
|
-
return out
|
|
256
|
-
|
|
257
|
-
|
|
258
239
|
def parse_reply(text: str, allowed: tuple[str, ...]) -> dict[str, Any]:
|
|
259
240
|
"""The agent's JSON, or a null verdict saying why not.
|
|
260
241
|
|
|
@@ -688,6 +669,15 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
688
669
|
else:
|
|
689
670
|
raise SystemExit("pass --model <path>, or --publisher with --package")
|
|
690
671
|
|
|
672
|
+
# What was measured, pinned by content. Coverage is sold as a per-version
|
|
673
|
+
# trend, and a trend needs each point tied to the bytes behind it: a
|
|
674
|
+
# `--model <dir>` run stamped `version: null` and named no path, so two
|
|
675
|
+
# runs reading 38% and 62% could not afterwards be told apart. The sha is
|
|
676
|
+
# over the same text the agent is shown, which is already in memory.
|
|
677
|
+
model_source = (str(a.model_path.resolve()) if a.model_path
|
|
678
|
+
else f"{a.publisher} {a.environment}/{a.package}")
|
|
679
|
+
model_sha = hashlib.sha256(model.encode()).hexdigest()
|
|
680
|
+
|
|
691
681
|
cases = [json.loads(l) for l in (a.set_dir / "cases.jsonl").read_text()
|
|
692
682
|
.splitlines() if l.strip()]
|
|
693
683
|
if a.only:
|
|
@@ -715,6 +705,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
715
705
|
if a.out:
|
|
716
706
|
a.out.write_text(json.dumps(
|
|
717
707
|
{"version": a.version, "set": str(a.set_dir),
|
|
708
|
+
"modelSource": model_source, "modelSha256": model_sha,
|
|
718
709
|
"agentModel": a.agent_model, **s, "cases_detail": rows,
|
|
719
710
|
**({"labelComparison": serialisable(cmp)} if cmp else {})},
|
|
720
711
|
indent=2))
|
|
@@ -404,5 +404,27 @@ class TheComparisonSurvivesTheArtifactWrite(unittest.TestCase):
|
|
|
404
404
|
# The console report still reads the tuple-keyed original.
|
|
405
405
|
self.assertIn("covered", cc.label_report(cmp))
|
|
406
406
|
|
|
407
|
+
class ModelIdentity(unittest.TestCase):
|
|
408
|
+
"""Coverage is sold as a per-version trend, and a trend needs each point
|
|
409
|
+
tied to the bytes behind it. A `--model <dir>` run stamped `version: null`
|
|
410
|
+
and named no path, so two runs reading 38% and 62% could not afterwards be
|
|
411
|
+
told apart -- the numbers became unciteable."""
|
|
412
|
+
|
|
413
|
+
SRC = (pathlib.Path(__file__).resolve().parent / "check_coverage.py").read_text()
|
|
414
|
+
|
|
415
|
+
def test_the_report_carries_the_model_source_and_sha(self):
|
|
416
|
+
self.assertIn('"modelSource": model_source', self.SRC)
|
|
417
|
+
self.assertIn('"modelSha256": model_sha', self.SRC)
|
|
418
|
+
|
|
419
|
+
def test_the_sha_is_over_the_text_the_agent_was_shown(self):
|
|
420
|
+
self.assertIn("hashlib.sha256(model.encode()).hexdigest()", self.SRC)
|
|
421
|
+
|
|
422
|
+
def test_a_local_run_names_the_resolved_path(self):
|
|
423
|
+
self.assertIn("str(a.model_path.resolve()) if a.model_path", self.SRC)
|
|
424
|
+
|
|
425
|
+
def test_a_served_run_names_the_publisher_and_package(self):
|
|
426
|
+
self.assertIn('f"{a.publisher} {a.environment}/{a.package}"', self.SRC)
|
|
427
|
+
|
|
428
|
+
|
|
407
429
|
if __name__ == "__main__":
|
|
408
430
|
unittest.main()
|