@malloy-publisher/skills 0.1.23 → 0.1.27
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 +9 -4
- package/skills/eval-answer/reference/coverage-limits.md +7 -4
- package/skills/eval-answer/reference/definition-ledger.md +2 -2
- package/skills/eval-answer/reference/ledger-schema.md +2 -1
- package/skills/eval-answer/scripts/check_coverage.py +175 -42
- package/skills/eval-answer/scripts/check_coverage_test.py +244 -52
- package/skills/eval-answer/scripts/init_truth_package.py +14 -1
- package/skills/eval-answer/scripts/ledger.py +1 -1
- package/skills/eval-answer/scripts/score_retrieval.py +4 -4
- package/skills/eval-answer/scripts/score_retrieval_test.py +8 -8
- package/skills/eval-answer/scripts/verify_goldens.py +217 -10
- package/skills/eval-answer/scripts/verify_goldens_test.py +237 -0
- package/skills/eval-diagnose/SKILL.md +6 -6
- package/skills/eval-diagnose/scripts/diagnose.py +3 -1
- package/skills/eval-diagnose/scripts/diagnose_test.py +12 -8
- package/skills/eval-import/SKILL.md +1 -1
- package/skills/eval-import/reference/case-format.md +19 -1
- package/skills/eval-improve/SKILL.md +1 -1
- package/skills/eval-judge/SKILL.md +25 -2
- package/skills/eval-judge/reference/writing-rubrics.md +8 -0
- package/skills/eval-loop/SKILL.md +1 -1
- package/skills/eval-loop/reference/running-a-run.md +4 -0
- package/skills/eval-loop/scripts/agent_harness.py +5 -0
- package/skills/eval-loop/scripts/agent_harness_test.py +3 -1
- package/skills/eval-loop/scripts/run_baseline.py +75 -3
- package/skills/eval-loop/scripts/run_baseline_test.py +92 -0
- package/skills/eval-report/SKILL.md +4 -4
- package/skills/malloy/SKILL.md +1 -1
- package/skills/malloy-analysis/SKILL.md +1 -1
- package/skills/malloy-analysis-pitfalls/SKILL.md +1 -1
- package/skills/malloy-analysis-report/SKILL.md +1 -1
- package/skills/malloy-analyze/SKILL.md +1 -1
- package/skills/malloy-charts/SKILL.md +1 -1
- package/skills/malloy-dashboards/SKILL.md +29 -31
- package/skills/malloy-debug/SKILL.md +1 -1
- package/skills/malloy-define/SKILL.md +1 -1
- package/skills/malloy-discover/SKILL.md +1 -1
- package/skills/malloy-document/SKILL.md +1 -1
- package/skills/malloy-getting-started/SKILL.md +1 -1
- package/skills/malloy-gotchas-modeling/SKILL.md +1 -1
- package/skills/malloy-gotchas-queries/SKILL.md +1 -1
- package/skills/malloy-gotchas-rendering/SKILL.md +1 -1
- package/skills/malloy-html-data-app-embedding/SKILL.md +3 -1
- package/skills/malloy-html-data-app-runtime/SKILL.md +1 -1
- package/skills/malloy-html-data-apps/SKILL.md +1 -1
- package/skills/malloy-lookml-review/SKILL.md +1 -1
- package/skills/malloy-materialization/SKILL.md +9 -7
- package/skills/malloy-materialization-tuning/SKILL.md +3 -2
- package/skills/malloy-model/SKILL.md +3 -4
- package/skills/malloy-model-as-you-go/SKILL.md +1 -1
- package/skills/malloy-modeling/SKILL.md +2 -2
- package/skills/malloy-notebook-chat/SKILL.md +1 -1
- package/skills/malloy-notebooks/SKILL.md +3 -1
- package/skills/malloy-patterns/SKILL.md +1 -1
- package/skills/malloy-phrase-detection/SKILL.md +1 -1
- package/skills/malloy-publish/SKILL.md +13 -12
- package/skills/malloy-queries/SKILL.md +1 -1
- package/skills/malloy-review/SKILL.md +1 -1
- package/skills/malloy-scope/SKILL.md +1 -1
- package/skills/malloy-source-unreachable/SKILL.md +14 -3
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.27
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Evaluate One Answer
|
|
@@ -146,12 +146,17 @@ recall is scoring search against an expectation that was never satisfiable.
|
|
|
146
146
|
Because it runs from the model text alone it is cheap enough to point at every
|
|
147
147
|
published version and read as a trend, which is what it is for. Its verdicts are
|
|
148
148
|
`eval-diagnose`'s codes verbatim, validated against that table at startup, so
|
|
149
|
-
`
|
|
150
|
-
they mean here. It writes nothing: not a golden, not the case-level `coverage`
|
|
149
|
+
`MISSING`, `AMBIGUOUS`, `RULE_UNWRITTEN` and `UNDERSPECIFIED` mean there exactly
|
|
150
|
+
what they mean here. A pass is `MODELLED`. It writes nothing: not a golden, not the case-level `coverage`
|
|
151
151
|
field, not a run directory. Where its verdict and that field disagree is where a
|
|
152
152
|
version regressed, and the field is the standing judgement about the question
|
|
153
153
|
while this is a measurement against one build.
|
|
154
154
|
|
|
155
|
+
Its `--out` report records `compiledSurface`: `read`, or why the compiled field
|
|
156
|
+
list was not read. Without that list the judge calls a column a source exposes
|
|
157
|
+
implicitly absent, so check the field before comparing two runs. A `--model`
|
|
158
|
+
run never has it.
|
|
159
|
+
|
|
155
160
|
How to invoke it. Either `--model <file-or-dir>` for a local package or
|
|
156
161
|
`--publisher <url> --package <pkg>` for a served one, plus `--set <dir>`, and
|
|
157
162
|
`--version <label>` to stamp the report so a trend has an x-axis:
|
|
@@ -199,7 +204,7 @@ purpose. Separately, the whole model goes into one prompt per case, so past a
|
|
|
199
204
|
few thousand lines it stops running on Linux and above about 100 KB the verdict
|
|
200
205
|
stops being stable. Do not take the over-size message's advice to narrow
|
|
201
206
|
`--model` to one file, which drops every imported source and manufactures
|
|
202
|
-
`
|
|
207
|
+
`MISSING` verdicts.
|
|
203
208
|
|
|
204
209
|
## Validate the definitions, not every answer
|
|
205
210
|
|
|
@@ -15,6 +15,10 @@ was. Separately, the whole model goes into one prompt per case, which caps the
|
|
|
15
15
|
package size it can run on at all and destabilises the verdict well before that
|
|
16
16
|
cap.
|
|
17
17
|
|
|
18
|
+
The measurements below predate the renamed verdicts, and their tables keep the
|
|
19
|
+
names they were taken under: `ok` is now `MODELLED`, `COVERAGE` is `MISSING`,
|
|
20
|
+
`NO-DISAMBIG` is merged into `AMBIGUOUS`, and `CONVENTION` is `RULE_UNWRITTEN`.
|
|
21
|
+
|
|
18
22
|
## Measured against a labelled set: the label drifts too
|
|
19
23
|
|
|
20
24
|
The `evals/ecommerce` set in `credibledata/malloy-samples` labels every case
|
|
@@ -158,7 +162,7 @@ concatenates every `*.malloy` under the tree and does **not** resolve `import`;
|
|
|
158
162
|
real packages split across files exactly that way (in
|
|
159
163
|
`credibledata/malloy-samples`, `ecommerce/brand_synergy.malloy` opens with
|
|
160
164
|
`import "ecommerce.malloy"`). Narrowing to one file drops every imported source,
|
|
161
|
-
so the judge correctly answers `
|
|
165
|
+
so the judge correctly answers `MISSING`, no entity represents the concept
|
|
162
166
|
anywhere, for cases the model answers perfectly well. That is a false gap
|
|
163
167
|
indistinguishable from a real one, and it lands in the numerator of a published
|
|
164
168
|
trend.
|
|
@@ -206,10 +210,9 @@ format address the cost, the ceiling and `json_objects()` together. Stdlib
|
|
|
206
210
|
`urllib` keeps the stdlib-only promise; the real cost is auth, since `claude -p`
|
|
207
211
|
rides the CLI's credentials.
|
|
208
212
|
|
|
209
|
-
**And for a large package, stop sending the whole model.** Rule out `
|
|
213
|
+
**And for a large package, stop sending the whole model.** Rule out `MISSING`
|
|
210
214
|
against a complete inventory of names and one-line docs, a small fraction of the
|
|
211
215
|
bytes, which keeps "no candidate anywhere" decidable; then send full text for
|
|
212
|
-
the candidate sources only, to settle `AMBIGUOUS
|
|
213
|
-
`CONVENTION`. Deliberately NOT retrieval ranking: folding retrieval into this
|
|
216
|
+
the candidate sources only, to settle `AMBIGUOUS` and `RULE_UNWRITTEN`. Deliberately NOT retrieval ranking: folding retrieval into this
|
|
214
217
|
number would collapse the distinction from recall that the metric exists to
|
|
215
218
|
draw.
|
|
@@ -136,7 +136,7 @@ record, and `verify_definitions.py` re-runs and compares it on every rebuild.
|
|
|
136
136
|
"query": "run: order_items -> { aggregate: control is sale_price.sum() { where: status != 'Cancelled' } - inventory_items.cost.sum() { where: status != 'Cancelled' } }",
|
|
137
137
|
"note": "excludes Cancelled, per status's own doc and units_sold"
|
|
138
138
|
},
|
|
139
|
-
"cause": "
|
|
139
|
+
"cause": "RULE_UNWRITTEN"
|
|
140
140
|
}
|
|
141
141
|
```
|
|
142
142
|
|
|
@@ -160,7 +160,7 @@ Authoring a control is judgement, and the same rules as authoring a golden apply
|
|
|
160
160
|
where two populations are both defensible from the docs, do not pick one --
|
|
161
161
|
record the ambiguity in `note` and leave the record `unchecked`. `cause` is the
|
|
162
162
|
`eval-diagnose` code the author expects a disagreement to mean (the cogs case is
|
|
163
|
-
`
|
|
163
|
+
`RULE_UNWRITTEN`); it is carried onto the finding, not inferred.
|
|
164
164
|
|
|
165
165
|
This is **validate what someone asserted**, not **find business-wrong definitions
|
|
166
166
|
unaided**. It turns "I believe `total_gross_margin` should exclude cancelled
|
|
@@ -124,6 +124,7 @@ on that axis.
|
|
|
124
124
|
| `diagnoserModel` / `improverModel` | Written by `diagnose.py` / `improve.py` when those stages run, so the run names every LLM that touched it. Absent on a run that was only answered and judged. |
|
|
125
125
|
| `modelGitSha` | Commit of the model repo, from `--model-repo`; `-dirty` suffix when that path had uncommitted changes -- fine for a band measurement, not for an A/B pin. **Null unless `--model-repo` names the repo.** It cannot be inferred: Publisher serves a COPY under `publisher_data/`, so the tree around the served file is the server's storage, not the model's history, and the field named that until 2026-09-03. `modelSha` above is the pin that always works; this is the pointer to where those bytes are versioned, and absent beats wrong. |
|
|
126
126
|
| `modelRepo` | The path `--model-repo` named, so a reader can tell an absent pin from an unrecorded one. |
|
|
127
|
+
| `modelDir` | The package directory `--model-dir` named, when it was: the path the `-dirty` marker on `modelGitSha` was decided over. Absent means the whole repo, where any unrelated untracked file marks the model dirty. |
|
|
127
128
|
| `environment` / `package` / `modelPath` | What was served, and from where. On a platform target `environment` is the organization and `package` is the workspace the MCP URL is scoped to. |
|
|
128
129
|
| `scope` | Platform target only: the `environment/package[@version]` the answerer was told to pass as an explicit `scopes` entry on every `get_context` / `execute_query` call. A workspace can serve many packages (a personal workspace serves every package the user can read), so without this the run also measures whether retrieval picks the right package -- a different measurement. Absent means unscoped. |
|
|
129
130
|
| `targetVersion` *(pin)* | Platform target only: the published version the answers came from. Both hosted tools default an omitted version to the PINNED one, meaning whatever the workspace serves at the moment of the call, so this only describes the run when `--scope` carries it to every call. `--target-version` supplies it when the scope omits it; a scope naming a different version is refused rather than treated as an override, because a run cannot say which build answered if two pins disagree. |
|
|
@@ -320,7 +321,7 @@ Run-level metrics fall out by counting:
|
|
|
320
321
|
|---|---|---|
|
|
321
322
|
| `issue_id` | string | Stable across status events. |
|
|
322
323
|
| `qids` | list | Affected cases. |
|
|
323
|
-
| `primary_code` / `contributing_codes` | string / list | From `skill:eval-diagnose`, verbatim. |
|
|
324
|
+
| `primary_code` / `contributing_codes` | string / list | From `skill:eval-diagnose`, verbatim. An event written before the codes were renamed may carry a retired one: `COVERAGE` is now `MISSING`, `NO-DISAMBIG` is `AMBIGUOUS`, and `CONVENTION` is `RULE_UNWRITTEN`. Read those as the new name; nothing rewrites old events. |
|
|
324
325
|
| `component` | string | `dataset` / `agent-call` / `get_context/model` / `get_context/retrieval` / `construction` / `model-definition`. |
|
|
325
326
|
| `owner` | string | `model` / `retrieval` / `agent-skill` / `dataset`. Environment failures stop the run; they are never diagnosed, so there is no environment owner. |
|
|
326
327
|
| `severity` / `confidence` | string | |
|
|
@@ -28,13 +28,13 @@ The verdicts are `eval-diagnose`'s cause codes, verbatim and validated against
|
|
|
28
28
|
its own table at startup, so a renamed code fails loudly here instead of
|
|
29
29
|
quietly meaning nothing:
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
AMBIGUOUS
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
MODELLED one entity expresses the concept, unambiguously
|
|
32
|
+
MISSING no query over this model could produce it
|
|
33
|
+
AMBIGUOUS several candidates and no doc says which
|
|
34
|
+
RULE_UNWRITTEN the data is there, the rule for combining it is not.
|
|
35
|
+
(guessable) when the data forces the rule, (arbitrary) when
|
|
36
|
+
it is a business decision nobody could derive
|
|
37
|
+
UNDERSPECIFIED the QUESTION is unclear, not the model
|
|
38
38
|
|
|
39
39
|
Exit 0 when the measurement ran, whatever it found. A low score is the result,
|
|
40
40
|
not a failure. Exit 1 if it could not run.
|
|
@@ -59,6 +59,7 @@ sys.path.insert(0, str(HERE))
|
|
|
59
59
|
|
|
60
60
|
from json_scan import json_objects # noqa: E402
|
|
61
61
|
from verify_goldens import model_text as local_model_text # noqa: E402
|
|
62
|
+
from check_findable import compiled_entities # noqa: E402
|
|
62
63
|
|
|
63
64
|
# The `claude -p` invocation lives once, in the harness, rather than being
|
|
64
65
|
# rewritten here. It costs this script a reach into a sibling skill, which the
|
|
@@ -85,9 +86,9 @@ JUDGE_BLOCKED = BLOCKED_TOOLS + ("Read", "Glob", "Grep", "Skill")
|
|
|
85
86
|
|
|
86
87
|
# Parsed from the table rather than typed here, the same way diagnose.py does
|
|
87
88
|
# it, so this cannot drift from the vocabulary it claims to reuse.
|
|
88
|
-
CODE_IN_TABLE = re.compile(r"^\|\s*`([A-Z][A-
|
|
89
|
-
FAIL_VERDICTS = ("
|
|
90
|
-
OK = "
|
|
89
|
+
CODE_IN_TABLE = re.compile(r"^\|\s*`([A-Z][A-Z_-]+)`\s*\|", re.M)
|
|
90
|
+
FAIL_VERDICTS = ("MISSING", "AMBIGUOUS", "RULE_UNWRITTEN", "UNDERSPECIFIED")
|
|
91
|
+
OK = "MODELLED"
|
|
91
92
|
|
|
92
93
|
# A prompt cap, not a limit anyone should hit. The model text goes in argv, and
|
|
93
94
|
# an over-long argv fails as an opaque OSError from the exec rather than as
|
|
@@ -148,6 +149,54 @@ def rest_model_text(base: str, environment: str, package: str,
|
|
|
148
149
|
return "\n\n".join(out)
|
|
149
150
|
|
|
150
151
|
|
|
152
|
+
def compiled_surface(base: str, environment: str, package: str) -> str:
|
|
153
|
+
"""The fields the COMPILED model exposes, which the source text does not name.
|
|
154
|
+
|
|
155
|
+
A Malloy source picks up every column of its table without declaring one of
|
|
156
|
+
them, so `products.retail_price` and `customers.signup_date` appear nowhere
|
|
157
|
+
in the `.malloy` and are fully queryable. A judge shown only source text
|
|
158
|
+
reads those as absent and returns MISSING on a question the model answers
|
|
159
|
+
-- measured, two of this skill's three false gaps on one set.
|
|
160
|
+
|
|
161
|
+
Empty string when the compiled model cannot be read, which is honest: the
|
|
162
|
+
prompt then says the list is unavailable rather than implying the text is
|
|
163
|
+
the whole surface.
|
|
164
|
+
"""
|
|
165
|
+
declared = compiled_entities(base, environment, package)
|
|
166
|
+
if not declared:
|
|
167
|
+
return ""
|
|
168
|
+
lines = []
|
|
169
|
+
for src in sorted(declared):
|
|
170
|
+
fields = sorted(f for f in declared[src] if not f.startswith("source:"))
|
|
171
|
+
if fields:
|
|
172
|
+
lines.append(f"{src}: " + ", ".join(fields))
|
|
173
|
+
return "\n".join(lines)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def set_conventions(set_dir: pathlib.Path) -> list[str]:
|
|
177
|
+
"""`set.json`'s `conventions`: what the questions' words mean to the business.
|
|
178
|
+
|
|
179
|
+
A list of strings. A bare string is taken as a list of one, and anything
|
|
180
|
+
else, or an unreadable file, is no conventions rather than a crash.
|
|
181
|
+
"""
|
|
182
|
+
f = set_dir / "set.json"
|
|
183
|
+
try:
|
|
184
|
+
conv = (json.loads(f.read_text()) or {}).get("conventions")
|
|
185
|
+
except (json.JSONDecodeError, OSError, AttributeError):
|
|
186
|
+
return []
|
|
187
|
+
if isinstance(conv, str):
|
|
188
|
+
conv = [conv]
|
|
189
|
+
return [c for c in conv if isinstance(c, str) and c.strip()] \
|
|
190
|
+
if isinstance(conv, list) else []
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# A convention is a definition of one TERM, scoped to the questions that use
|
|
194
|
+
# it. Unscoped, the judge applied every definition to every question: nine of
|
|
195
|
+
# twelve cases came back as gaps. The sentence is prose, so the check is only
|
|
196
|
+
# that one exists.
|
|
197
|
+
CONVENTION_SCOPE = re.compile(r"(?i)\bapplies to\b")
|
|
198
|
+
|
|
199
|
+
|
|
151
200
|
# --- the judgement -----------------------------------------------------------
|
|
152
201
|
|
|
153
202
|
PROMPT = """You are measuring whether a semantic model can EXPRESS an answer to
|
|
@@ -157,21 +206,59 @@ there are no rows here and no way to run anything. Judge the model text only.
|
|
|
157
206
|
THE MODEL (every source, dimension, measure and doc comment it defines):
|
|
158
207
|
{model}
|
|
159
208
|
|
|
209
|
+
FIELDS THE COMPILED MODEL EXPOSES: {surface}
|
|
210
|
+
|
|
211
|
+
A Malloy source exposes every column of its table whether or not the text
|
|
212
|
+
declares it, so this list is the authority on what EXISTS and the text above is
|
|
213
|
+
the authority on what is DOCUMENTED. A field here but not in the text is real
|
|
214
|
+
and queryable -- it is undocumented, not absent, and undocumented is not
|
|
215
|
+
`MISSING`. When this list is unavailable, say so in your reasoning and do not
|
|
216
|
+
treat the text as the whole surface.
|
|
217
|
+
|
|
160
218
|
THE QUESTION: {question}
|
|
161
219
|
|
|
220
|
+
WHAT THE BUSINESS MEANS BY IT: {conventions}
|
|
221
|
+
|
|
222
|
+
These are definitions of TERMS, not defaults for every question. Apply one only
|
|
223
|
+
when THIS question uses the term it defines, or asks for a quantity that cannot
|
|
224
|
+
be computed without it. A definition of "net" governs a question that says net;
|
|
225
|
+
it does not make every revenue question a net question. A definition of
|
|
226
|
+
"customer" governs a question that counts customers; it does not govern a
|
|
227
|
+
question about categories that happens to involve people. Check, for each
|
|
228
|
+
convention, whether this question actually invokes it, and ignore the ones it
|
|
229
|
+
does not -- applying them all to everything turns every question into a gap and
|
|
230
|
+
the measurement stops discriminating.
|
|
231
|
+
|
|
232
|
+
Where the question DOES invoke one, it is not a reading you may substitute a
|
|
233
|
+
more reasonable one for. If the convention names a window, a basis or a filter
|
|
234
|
+
that no entity in the model expresses, the model cannot answer the question
|
|
235
|
+
however well its parts are documented: `RULE_UNWRITTEN` when the underlying data
|
|
236
|
+
is present, `MISSING` when it is not. Judging by your own reading of a word the
|
|
237
|
+
business has already defined is how this check passes a question the model
|
|
238
|
+
demonstrably cannot answer.
|
|
239
|
+
|
|
162
240
|
CONCEPTS THE QUESTION NEEDS: {concepts}
|
|
163
241
|
|
|
164
242
|
Decide ONE verdict:
|
|
165
243
|
|
|
166
|
-
- `
|
|
167
|
-
- `
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
- `
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
244
|
+
- `MODELLED` -- one entity expresses it, unambiguously. Name that entity.
|
|
245
|
+
- `MISSING` -- no query over this model could produce the concept. Not merely
|
|
246
|
+
that no measure is named for it: if the parts are present and only the
|
|
247
|
+
formula is absent, that is `RULE_UNWRITTEN`.
|
|
248
|
+
- `AMBIGUOUS` -- several candidates and nothing says which this question means.
|
|
249
|
+
Judge the entities AND their docs: a `#(doc)` that resolves the choice makes
|
|
250
|
+
it `MODELLED`.
|
|
251
|
+
- `RULE_UNWRITTEN` -- the data is present and the model does not encode the
|
|
252
|
+
rule for combining or filtering it, so whoever answers invents one. Say which
|
|
253
|
+
kind in `rule_kind`: `guessable` when the data forces the rule (list price
|
|
254
|
+
minus sale price is a discount, and any careful reader gets it), `arbitrary`
|
|
255
|
+
when it is a business decision nobody could derive (a season window, whether
|
|
256
|
+
revenue is net of tax). If a stated convention above names the rule and the
|
|
257
|
+
model does not encode it, that is `arbitrary` -- you know the rule only
|
|
258
|
+
because you were told.
|
|
259
|
+
- `UNDERSPECIFIED` -- the QUESTION does not say what it means, so no model
|
|
260
|
+
could answer it as written. "Adjusted sales" with no statement of the
|
|
261
|
+
adjustment. Do not score this against the model.
|
|
175
262
|
|
|
176
263
|
Work in this order. The enumeration is the job; skipping it is how this
|
|
177
264
|
judgement goes wrong.
|
|
@@ -197,16 +284,14 @@ obvious the answer feels.
|
|
|
197
284
|
|
|
198
285
|
Then the verdict follows mechanically:
|
|
199
286
|
|
|
200
|
-
- Every quantity has exactly one candidate, or the model states which: `
|
|
201
|
-
- Some quantity
|
|
202
|
-
- A quantity has several
|
|
203
|
-
- A quantity has two plausible candidates and nothing resolves which:
|
|
204
|
-
`NO-DISAMBIG`.
|
|
287
|
+
- Every quantity has exactly one candidate, or the model states which: `MODELLED`.
|
|
288
|
+
- Some quantity cannot be produced by any query over this model: `MISSING`.
|
|
289
|
+
- A quantity has several candidates and nothing resolves which: `AMBIGUOUS`.
|
|
205
290
|
- The parts exist but the model names no measure for the combination the
|
|
206
291
|
question asks for, so whoever answers must assemble it and choose a
|
|
207
|
-
convention: `
|
|
292
|
+
convention: `RULE_UNWRITTEN`.
|
|
208
293
|
|
|
209
|
-
Three rules about that, because each is a way to reach `
|
|
294
|
+
Three rules about that, because each is a way to reach `MODELLED` wrongly:
|
|
210
295
|
|
|
211
296
|
1. **A question's words resembling a field's label is not the model resolving
|
|
212
297
|
anything.** A question saying "total universe" and a measure labelled
|
|
@@ -214,8 +299,8 @@ Three rules about that, because each is a way to reach `ok` wrongly:
|
|
|
214
299
|
also defensible and would give a materially different number, the model has
|
|
215
300
|
not resolved it and you must not resolve it yourself.
|
|
216
301
|
2. **Finding the numerator is not coverage.** If the numerator is named and the
|
|
217
|
-
denominator must be assembled or chosen, that is `
|
|
218
|
-
`
|
|
302
|
+
denominator must be assembled or chosen, that is `RULE_UNWRITTEN` or
|
|
303
|
+
`AMBIGUOUS`, never `MODELLED`. Matching field names against the question is the
|
|
219
304
|
failure mode this measurement exists to remove.
|
|
220
305
|
3. **A caveat is not a resolution.** A doc warning that a measure is easy to
|
|
221
306
|
misuse still leaves the question open unless it says which reading this
|
|
@@ -227,12 +312,13 @@ Return ONLY a JSON object, no prose around it, keys in this order:
|
|
|
227
312
|
"ruled_out": {{"<quantity>": "<the doc sentence eliminating the others, or null>"}},
|
|
228
313
|
"resolved_by": "what in the model says which candidate, or null",
|
|
229
314
|
"why": "one or two sentences naming the specific entity or the specific gap",
|
|
230
|
-
"verdict": "
|
|
315
|
+
"verdict": "MODELLED|MISSING|AMBIGUOUS|RULE_UNWRITTEN|UNDERSPECIFIED",
|
|
316
|
+
"rule_kind": "guessable|arbitrary, only when verdict is RULE_UNWRITTEN, else null",
|
|
231
317
|
"entities": ["entity names that express it, or [] when it is not expressible"]}}
|
|
232
318
|
|
|
233
319
|
The enumeration comes first because the verdict follows from it. Before you emit
|
|
234
320
|
`verdict`, re-read `quantities` and `ruled_out`: if any quantity holds more than
|
|
235
|
-
one candidate and its `ruled_out` entry is null, the verdict is not `
|
|
321
|
+
one candidate and its `ruled_out` entry is null, the verdict is not `MODELLED`.
|
|
236
322
|
"""
|
|
237
323
|
|
|
238
324
|
|
|
@@ -271,7 +357,7 @@ def parse_reply(text: str, allowed: tuple[str, ...]) -> dict[str, Any]:
|
|
|
271
357
|
unresolved = [q for q, c in quantities.items()
|
|
272
358
|
if isinstance(c, list) and len(c) > 1]
|
|
273
359
|
if unresolved:
|
|
274
|
-
return {"verdict": "
|
|
360
|
+
return {"verdict": "AMBIGUOUS",
|
|
275
361
|
"why": f"[several candidates for {', '.join(unresolved)} "
|
|
276
362
|
f"and nothing in the model resolves them] {why}",
|
|
277
363
|
"entities": ents, "quantities": quantities,
|
|
@@ -280,10 +366,18 @@ def parse_reply(text: str, allowed: tuple[str, ...]) -> dict[str, Any]:
|
|
|
280
366
|
# whole value of this metric is that a pass can be checked against the model.
|
|
281
367
|
if got == OK and not ents:
|
|
282
368
|
return {"verdict": None,
|
|
283
|
-
"why": "
|
|
369
|
+
"why": "MODELLED without naming an entity that expresses it",
|
|
284
370
|
"entities": [], "quantities": quantities,
|
|
285
371
|
"resolved_by": resolved_by}
|
|
286
|
-
|
|
372
|
+
# `arbitrary` is the consequential half and the one a judge under-reports,
|
|
373
|
+
# so an unqualified RULE_UNWRITTEN is recorded as unknown rather than
|
|
374
|
+
# silently counted as the harmless kind.
|
|
375
|
+
kind = v.get("rule_kind")
|
|
376
|
+
if got != "RULE_UNWRITTEN":
|
|
377
|
+
kind = None
|
|
378
|
+
elif kind not in ("guessable", "arbitrary"):
|
|
379
|
+
kind = "unstated"
|
|
380
|
+
return {"verdict": got, "why": why, "entities": ents, "rule_kind": kind,
|
|
287
381
|
"quantities": quantities, "resolved_by": resolved_by}
|
|
288
382
|
|
|
289
383
|
|
|
@@ -294,9 +388,9 @@ def majority(rows: list[dict[str, Any]]) -> dict[str, Any]:
|
|
|
294
388
|
is a property of the case rather than noise to average away. One sample
|
|
295
389
|
cannot tell a stable judgement from a marginal one, so a repeated run
|
|
296
390
|
records both the verdict and what the samples actually were. (The fixture
|
|
297
|
-
below is not such a case: measured 2026-09-08 it read
|
|
391
|
+
below is not such a case: measured 2026-09-08 it read RULE_UNWRITTEN three times
|
|
298
392
|
out of three. An EARLIER fixture, replaced when a customer model excerpt was
|
|
299
|
-
scrubbed from this file, read
|
|
393
|
+
scrubbed from this file, read RULE_UNWRITTEN twice and `MODELLED` once, which is where
|
|
300
394
|
the tie rule came from.)
|
|
301
395
|
"""
|
|
302
396
|
counts: dict[str, int] = {}
|
|
@@ -329,13 +423,16 @@ def majority(rows: list[dict[str, Any]]) -> dict[str, Any]:
|
|
|
329
423
|
def judge_case(case: dict[str, Any], model: str, a: argparse.Namespace,
|
|
330
424
|
allowed: tuple[str, ...]) -> dict[str, Any]:
|
|
331
425
|
concepts = case.get("requiresConcepts") or []
|
|
332
|
-
prompt = PROMPT.format(model=model,
|
|
426
|
+
prompt = PROMPT.format(model=model,
|
|
427
|
+
surface=getattr(a, "surface", "") or "(unavailable)",
|
|
428
|
+
conventions=getattr(a, "conventions", "") or "(none stated)",
|
|
429
|
+
question=case.get("question", ""),
|
|
333
430
|
concepts=", ".join(concepts) or "(none named)")
|
|
334
431
|
if len(prompt) > MAX_PROMPT:
|
|
335
432
|
# The way out depends on the mode. `--model-path` narrows a
|
|
336
433
|
# `--publisher` read to one model. A local `--model` read has no such
|
|
337
434
|
# move: `model_text()` does not resolve `import`, so pointing it at one
|
|
338
|
-
# file drops every imported source and the judge answers
|
|
435
|
+
# file drops every imported source and the judge answers MISSING for
|
|
339
436
|
# concepts the model does represent. That is a false gap that lands in
|
|
340
437
|
# a published trend (reference/coverage-limits.md), and this message
|
|
341
438
|
# used to recommend exactly that. The honest outcome is a failed
|
|
@@ -343,7 +440,7 @@ def judge_case(case: dict[str, Any], model: str, a: argparse.Namespace,
|
|
|
343
440
|
narrow = ("pass --model-path to measure one model file" if a.publisher
|
|
344
441
|
else "this is a failed measurement, not a number to rescue: "
|
|
345
442
|
"do NOT narrow --model to one file, that drops imported "
|
|
346
|
-
"sources and manufactures
|
|
443
|
+
"sources and manufactures MISSING verdicts "
|
|
347
444
|
"(reference/coverage-limits.md). Record coverage as "
|
|
348
445
|
"not measured for this package version")
|
|
349
446
|
return {"qid": case["qid"], "verdict": None,
|
|
@@ -551,10 +648,10 @@ FIXTURE_CASE = {
|
|
|
551
648
|
"question": "What was our first contact resolution rate last quarter?",
|
|
552
649
|
"requiresConcepts": ["first contact resolution", "ticket population"],
|
|
553
650
|
}
|
|
554
|
-
FIXTURE_EXPECTED = ("
|
|
651
|
+
FIXTURE_EXPECTED = ("RULE_UNWRITTEN", "AMBIGUOUS")
|
|
555
652
|
# Measured against THIS fixture on 2026-09-08, agent model sonnet, --repeat 3:
|
|
556
|
-
# fixture_first_contact_resolution_rate:
|
|
557
|
-
# (samples: CONVENTION, CONVENTION, CONVENTION)
|
|
653
|
+
# fixture_first_contact_resolution_rate: RULE_UNWRITTEN
|
|
654
|
+
# (samples: CONVENTION, CONVENTION, CONVENTION -- the retired name)
|
|
558
655
|
# Re-measure when the fixture, the prompt or the verdict vocabulary changes.
|
|
559
656
|
# The tests below pin the fixture's SHAPE and cannot pin its verdict: the judge
|
|
560
657
|
# needs a live `claude -p`, so `--self-check` is the only thing that measures
|
|
@@ -669,6 +766,41 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
669
766
|
else:
|
|
670
767
|
raise SystemExit("pass --model <path>, or --publisher with --package")
|
|
671
768
|
|
|
769
|
+
# The compiled surface names the columns a source exposes without declaring
|
|
770
|
+
# them. Only REST can supply it; a --model run reads text off disk and has
|
|
771
|
+
# no compiled model to ask, and the prompt is told so rather than being let
|
|
772
|
+
# to assume the text is everything.
|
|
773
|
+
#
|
|
774
|
+
# Whether it was read goes into the report, not only onto stderr. A
|
|
775
|
+
# --publisher run whose REST read failed is measuring with less than the
|
|
776
|
+
# judge is told it has, and without the record it reads as a normal run.
|
|
777
|
+
a.surface = ""
|
|
778
|
+
surface_status = "not read: --model has no compiled model to ask"
|
|
779
|
+
if a.publisher and a.package:
|
|
780
|
+
try:
|
|
781
|
+
a.surface = compiled_surface(a.publisher, a.environment, a.package)
|
|
782
|
+
surface_status = ("read" if a.surface else
|
|
783
|
+
"empty: the server returned no compiled fields")
|
|
784
|
+
except (urllib.error.URLError, OSError, json.JSONDecodeError) as exc:
|
|
785
|
+
surface_status = f"failed: {exc}"[:200]
|
|
786
|
+
if not a.surface:
|
|
787
|
+
print(f"! no compiled field list ({surface_status}): a column a source "
|
|
788
|
+
f"exposes implicitly will read as absent", file=sys.stderr)
|
|
789
|
+
|
|
790
|
+
# What the question's words MEAN, from the set. A convention the model does
|
|
791
|
+
# not encode is the thing this check exists to find, and a judge that does
|
|
792
|
+
# not know the business defines "summer" as a date window will substitute
|
|
793
|
+
# its own reading and pass the question.
|
|
794
|
+
conventions = set_conventions(a.set_dir)
|
|
795
|
+
a.conventions = "\n".join(f"- {c}" for c in conventions)
|
|
796
|
+
for i, c in enumerate(conventions, 1):
|
|
797
|
+
if not CONVENTION_SCOPE.search(c):
|
|
798
|
+
print(f"! set.json conventions[{i - 1}] states no scope. Without "
|
|
799
|
+
f"an \"Applies to questions that ...\" sentence the judge "
|
|
800
|
+
f"reads it as a rule for every question. Fix: end it with "
|
|
801
|
+
f"\"Applies to questions that ask about <term>.\"",
|
|
802
|
+
file=sys.stderr)
|
|
803
|
+
|
|
672
804
|
# What was measured, pinned by content. Coverage is sold as a per-version
|
|
673
805
|
# trend, and a trend needs each point tied to the bytes behind it: a
|
|
674
806
|
# `--model <dir>` run stamped `version: null` and named no path, so two
|
|
@@ -706,7 +838,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
706
838
|
a.out.write_text(json.dumps(
|
|
707
839
|
{"version": a.version, "set": str(a.set_dir),
|
|
708
840
|
"modelSource": model_source, "modelSha256": model_sha,
|
|
709
|
-
"agentModel": a.agent_model,
|
|
841
|
+
"agentModel": a.agent_model, "compiledSurface": surface_status,
|
|
842
|
+
"conventions": conventions, **s, "cases_detail": rows,
|
|
710
843
|
**({"labelComparison": serialisable(cmp)} if cmp else {})},
|
|
711
844
|
indent=2))
|
|
712
845
|
print(f"\n{a.out}")
|