@malloy-publisher/skills 0.1.10 → 0.1.13

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 (82) hide show
  1. package/package.json +1 -1
  2. package/skills/eval-answer/SKILL.md +162 -0
  3. package/skills/eval-answer/reference/ledger-schema.md +324 -0
  4. package/skills/eval-answer/scripts/check_contamination.py +213 -0
  5. package/skills/eval-answer/scripts/check_contamination_test.py +128 -0
  6. package/skills/eval-answer/scripts/init_truth_package.py +216 -0
  7. package/skills/eval-answer/scripts/init_truth_package_test.py +48 -0
  8. package/skills/eval-answer/scripts/ledger.py +386 -0
  9. package/skills/eval-answer/scripts/ledger_test.py +134 -0
  10. package/skills/eval-answer/scripts/mcp_payload.py +212 -0
  11. package/skills/eval-answer/scripts/mcp_payload_test.py +219 -0
  12. package/skills/eval-answer/scripts/publisher_rest.py +172 -0
  13. package/skills/eval-answer/scripts/publisher_rest_test.py +78 -0
  14. package/skills/eval-answer/scripts/score_retrieval.py +363 -0
  15. package/skills/eval-answer/scripts/score_retrieval_test.py +328 -0
  16. package/skills/eval-answer/scripts/verify_goldens.py +389 -0
  17. package/skills/eval-diagnose/SKILL.md +248 -0
  18. package/skills/eval-diagnose/reference/output-contract.md +47 -0
  19. package/skills/eval-diagnose/scripts/cluster_failures.py +151 -0
  20. package/skills/eval-diagnose/scripts/diagnose.py +481 -0
  21. package/skills/eval-improve/SKILL.md +181 -0
  22. package/skills/eval-improve/reference/output-contract.md +27 -0
  23. package/skills/eval-improve/scripts/improve.py +425 -0
  24. package/skills/eval-judge/SKILL.md +180 -0
  25. package/skills/eval-judge/reference/refusal.md +101 -0
  26. package/skills/eval-judge/reference/retrieval-judge.md +41 -0
  27. package/skills/eval-judge/reference/suspect-goldens.md +39 -0
  28. package/skills/eval-judge/reference/writing-rubrics.md +55 -0
  29. package/skills/eval-loop/SKILL.md +350 -0
  30. package/skills/eval-loop/reference/acceptance-check.md +58 -0
  31. package/skills/eval-loop/reference/checking-the-judge.md +31 -0
  32. package/skills/eval-loop/reference/golden-side-door.md +137 -0
  33. package/skills/eval-loop/reference/measurement.md +120 -0
  34. package/skills/eval-loop/reference/running-a-run.md +82 -0
  35. package/skills/eval-loop/scripts/agent_harness.py +434 -0
  36. package/skills/eval-loop/scripts/build_run_package.py +509 -0
  37. package/skills/eval-loop/scripts/check_judge.py +183 -0
  38. package/skills/eval-loop/scripts/flip_table.py +315 -0
  39. package/skills/eval-loop/scripts/platform_target_test.py +101 -0
  40. package/skills/eval-loop/scripts/run_baseline.py +1491 -0
  41. package/skills/eval-loop/scripts/serve.py +130 -0
  42. package/skills/eval-loop/templates/eval-run-package/README.md +183 -0
  43. package/skills/eval-loop/templates/eval-run-package/eval_run.malloy +757 -0
  44. package/skills/eval-loop/templates/eval-run-package/eval_run.malloynb +160 -0
  45. package/skills/eval-loop/templates/eval-run-package/public/app.js +406 -0
  46. package/skills/eval-loop/templates/eval-run-package/public/index.html +215 -0
  47. package/skills/eval-loop/templates/eval-run-package/publisher.json +5 -0
  48. package/skills/malloy/SKILL.md +13 -2
  49. package/skills/malloy-analysis/SKILL.md +1 -1
  50. package/skills/malloy-analysis-pitfalls/SKILL.md +1 -1
  51. package/skills/malloy-analysis-report/SKILL.md +2 -2
  52. package/skills/malloy-analyze/SKILL.md +1 -1
  53. package/skills/malloy-charts/SKILL.md +1 -1
  54. package/skills/malloy-dashboards/SKILL.md +1 -1
  55. package/skills/malloy-debug/SKILL.md +9 -6
  56. package/skills/malloy-define/SKILL.md +1 -1
  57. package/skills/malloy-discover/SKILL.md +1 -1
  58. package/skills/malloy-document/SKILL.md +1 -1
  59. package/skills/malloy-getting-started/SKILL.md +51 -1
  60. package/skills/malloy-gotchas-modeling/SKILL.md +42 -2
  61. package/skills/malloy-gotchas-queries/SKILL.md +1 -1
  62. package/skills/malloy-gotchas-rendering/SKILL.md +1 -1
  63. package/skills/malloy-html-data-app-embedding/SKILL.md +1 -1
  64. package/skills/malloy-html-data-app-runtime/SKILL.md +1 -1
  65. package/skills/malloy-html-data-apps/SKILL.md +1 -1
  66. package/skills/malloy-lookml-review/SKILL.md +1 -1
  67. package/skills/malloy-lookml-review/reference/_concepts.md +1 -1
  68. package/skills/malloy-lookml-review/reference/build-derived-tables.md +5 -3
  69. package/skills/malloy-materialization/SKILL.md +19 -4
  70. package/skills/malloy-materialization-tuning/SKILL.md +1 -1
  71. package/skills/malloy-model/SKILL.md +4 -2
  72. package/skills/malloy-model-as-you-go/SKILL.md +1 -1
  73. package/skills/malloy-modeling/SKILL.md +1 -1
  74. package/skills/malloy-notebook-chat/SKILL.md +2 -2
  75. package/skills/malloy-notebooks/SKILL.md +1 -1
  76. package/skills/malloy-patterns/SKILL.md +1 -1
  77. package/skills/malloy-phrase-detection/SKILL.md +1 -1
  78. package/skills/malloy-publish/SKILL.md +1 -1
  79. package/skills/malloy-queries/SKILL.md +1 -1
  80. package/skills/malloy-review/SKILL.md +1 -1
  81. package/skills/malloy-review/reference/rubric-structure.md +1 -1
  82. package/skills/malloy-scope/SKILL.md +1 -1
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.10",
4
+ "version": "0.1.13",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
@@ -0,0 +1,162 @@
1
+ ---
2
+ name: eval-answer
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
5
+ ---
6
+
7
+ # Evaluate One Answer
8
+
9
+ One user intent, answered once. This skill decides whether that answer was
10
+ correct, records the evidence, and stops.
11
+
12
+ **Scope boundary:** verdict and events only. No diagnosis, no model edit.
13
+
14
+ ## The unit
15
+
16
+ A chat is not the unit. Segment by user intent. Feedback ("break it out by
17
+ region") is a revision inside the same answer; grade the final accepted
18
+ revision.
19
+
20
+ Take the question from the stored case (`evals/<set>/cases.jsonl`), never from
21
+ memory or a truncated console line. Record `question_sha` of the exact text
22
+ the answerer saw. Record `servedRevision` from `get_context` or reload, not
23
+ the package name: a same-named decoy has been measured for hours.
24
+
25
+ ## Step 1: Contamination check, before any score
26
+
27
+ The answerer can Read or Shell its way to gold. Publisher traces do not see
28
+ that, so the check runs on the HOST-side tool-use log you kept for the
29
+ answerer subagent (every tool name and its path or command), plus the MCP
30
+ call counts the answerer reported.
31
+
32
+ The checklist. An attempt is contaminated when its log shows any of:
33
+
34
+ 1. a Read, Shell, or any file tool touching `evals/` or a gold artifact path;
35
+ 2. any access to the model file under test through a file tool (the
36
+ `modelPath` argument on an MCP `execute_query` is NOT contamination; the
37
+ server resolves it, the answerer never reads the file);
38
+ 3. `reported_calls` greater than `host_tool_uses` (the detectable
39
+ under-report floor is reported at most total tool uses).
40
+
41
+ `skills/eval-answer/scripts/check_contamination.py` is a reference aid that
42
+ mechanizes the same checklist over a JSON log; your reading of the transcript
43
+ is the check, the script is a second pair of eyes.
44
+
45
+ Contaminated attempts get `verdict: null` and `contaminated: true`. They are
46
+ excluded from the run aggregates. They are not "wrong answers."
47
+
48
+ If you cannot produce a host log, mark `contaminated: "unknown"` on both the
49
+ attempt and its score event, and do not treat the attempt as a clean pass.
50
+
51
+ ## Step 2: Re-run the submitted query yourself
52
+
53
+ Never score the agent's reported rows. Take its final query, execute it with
54
+ `execute_query`, and write a prediction CSV under the run's
55
+ `artifacts/` directory.
56
+
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.
61
+
62
+ ## Step 3: Judge the answer
63
+
64
+ Spawn one fresh judge subagent per attempt, following
65
+ `skill:eval-judge` (the rubric, the anchors, and the output shape live
66
+ there; this skill does not restate them). Give it the question, the golden,
67
+ your re-executed prediction rows, the canonical query when present, and the
68
+ relevant source and field definitions from the model. It returns
69
+ `{verdict, confidence, why, column_pairing}`.
70
+
71
+ - The judge sees gold. It is therefore never the answerer, and its verdict
72
+ never leaks back to any answerer.
73
+ - Confidence 5 or lower records as `needs_human`: neither a pass nor a fail,
74
+ excluded from acceptance arithmetic, queued for a human look.
75
+ - `near_match` is also neither. It means defensibly different, not "nearly a
76
+ pass", and it stays out of the pass rate and the acceptance check for the same reason
77
+ `needs_human` does. Report the count; do not fold it into either column.
78
+ - When a human overrules a verdict, append the case to
79
+ `evals/<set>/judge-regressions.jsonl`.
80
+ - For a scalar golden, the same protocol applies to a one-value prediction.
81
+ For `unanswerable`, a refusal that names the gap is the pass; a confident
82
+ numeric answer is the fail.
83
+ - Large row sets are still the judge's job. There is no scripted row oracle:
84
+ a script that can pass a wrong answer is worse than none, and the rubric's
85
+ containment and column-pairing rules are what the comparison needs.
86
+
87
+ ## Step 4: Score what retrieval delivered
88
+
89
+ Per attempt, mechanically, from the ledger -- `scripts/score_retrieval.py`. Each
90
+ case names the entities its answer depends on (`expectedEntities.required`, and
91
+ `requiredAnyOf` groups where the model offers more than one route). An entity
92
+ was delivered if the attempt's `get_context` calls returned it as a ranked entity
93
+ under its id, under the same type and name on a sibling source, or by name inside
94
+ a returned source's documentation -- text the answerer reads and acts on. Only
95
+ `missing` is a retrieval miss; the route per entity is recorded so the strict
96
+ count is still there.
97
+
98
+ Recall 1.0 with a wrong answer exonerates retrieval: the failure is in the query.
99
+ Recall below 1.0 and `coverage: covered` means the entity existed and search did
100
+ not surface it; `derivable` or `absent` means there was nothing to surface. Those
101
+ look identical in an answer score and have opposite owners, which is what makes
102
+ this number worth having. It uses the search terms the answerer chose, so it
103
+ attributes a failure *within* an arm and does not compare retrieval across arms
104
+ -- that is the engine-side `eval-retrieval` skill, which does not ship here.
105
+
106
+ ## Step 5: Distrust the golden
107
+
108
+ A reference answer can be wrong (parent-column fanout, a join on a shared
109
+ non-identifying key, or a rubric describing a model that has since been fixed).
110
+ Fanout is not automatically a defect: `AVG` / `STDDEV` / `MIN` / `MAX` survive
111
+ uniform duplication. Classify `verified_wrong` (exclude from scoring) vs
112
+ `verified_benign` (keep).
113
+
114
+ **The judge produces this, not you.** It is the only station holding the golden,
115
+ the re-executed rows and the model source at once, so it is the only one that can
116
+ see the key contradict any of them; the rules and the four values are in
117
+ `skill:eval-judge`. Carry its `gold_status` and `gold_note` onto the score
118
+ event unchanged, and where it says nothing, fall back to the case's standing
119
+ `golden.status`.
120
+
121
+ Do not encode a rewrite of a bad golden into the model. A `suspect` or
122
+ `verified_wrong`, or a no_match whose why indicts the golden rather than the
123
+ 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 --
125
+ otherwise a modelling agent is dispatched to fix a model that is already right.
126
+
127
+ ## Step 6: Append events, then stop
128
+
129
+ Append to `evals/<set>/runs/<runId>/events.jsonl` with `caseId` set. Shapes
130
+ live in `reference/ledger-schema.md`.
131
+
132
+ 1. `attempt`: qid, sample, phase, question_sha, submitted, final_query,
133
+ served revision, call counts, contamination verdict, transcript path.
134
+ 2. `tool_call`: one per MCP `get_context` / `execute_query`, with `traceId`
135
+ and the `rankedSummary` copied from the trace (per-target ranks included).
136
+ Do not copy full traces into the event; the trace store holds the body.
137
+ 3. `score`: the judge's verdict object plus `judge_version`, `rubric_sha`,
138
+ `golden_revision`, `contaminated`, `gold_status`, and the judge output's
139
+ artifact path.
140
+
141
+ A stage never rewrites another stage's fields. End-of-run numbers come from
142
+ counting events, not from your arithmetic in prose.
143
+
144
+ Sample each case once. Breadth across cases beats repeats of one case; the
145
+ comparison rule for a before/after is the flip count in `skill:eval-loop`
146
+ Measurement, not a mean over samples.
147
+
148
+ ## Re-score after a golden repair
149
+
150
+ When `eval-loop` has repaired a golden and opened a new run, this skill runs
151
+ again **without a new answerer**: same stored `final_query` (or its saved
152
+ prediction CSV), new gold artifact, fresh judge, new `golden_revision` on the
153
+ `score` event. Contamination does not need to be re-litigated if the attempt
154
+ was already clean. If you must re-execute, do it yourself; do not ask the
155
+ original answerer to "try again" with the new key in context.
156
+
157
+ ## Related skills
158
+
159
+ - `skill:eval-diagnose`: why it failed, after this record exists.
160
+ - `skill:eval-improve`: smallest model edit, model-owned issues only.
161
+ - The `malloy-analysis-pitfalls` skill: checks before you trust a result you ran.
162
+ The judge loads it by name; it is not part of the `eval` group.
@@ -0,0 +1,324 @@
1
+ # The eval ledger: files and events
2
+
3
+ The ledger is plain files in the model package's git repository. There is no
4
+ eval API and no eval database. The conductor (`skill:eval-loop`) reads and
5
+ writes these files directly; the stages share them as their contract.
6
+ `eval-answer` writes `attempt`, `tool_call`, and `score`. An engine-side
7
+ `eval-retrieval` skill, which does not ship here, writes `retrieval_score` and
8
+ `probe`; both stay in this contract so one validator covers every run
9
+ directory.
10
+ `eval-diagnose` writes `issue` and `issue_status`. `eval-improve` writes
11
+ `candidate`. `eval-loop` writes `acceptance_check`, further `issue_status`, and
12
+ `checkpoint`.
13
+
14
+ **The contract is code: `eval-answer/scripts/ledger.py`.** Every script that
15
+ writes `run.json` or `events.jsonl` builds its lines through that module, so a
16
+ field rename that reaches only one writer fails at write time. This document
17
+ is the human-readable rendering; where the two disagree, the module is right
18
+ and this file is the bug. Check any run directory with:
19
+
20
+ ```
21
+ python3 skills/eval-answer/scripts/ledger.py validate <runDir>
22
+ ```
23
+
24
+ Errors are broken identity (missing `run.json`, a missing required field, two
25
+ scores for one attempt); warnings are missing comparison pins (a run without
26
+ `skillsVersion` still measures, but cannot anchor an A/B on that pin) and
27
+ grandfathered unknown fields on old runs.
28
+
29
+ ## Layout
30
+
31
+ ```
32
+ evals/<set>/
33
+ set.json # set metadata (below)
34
+ cases.jsonl # one case per line
35
+ judge-regressions.jsonl # judge verdicts a human overruled
36
+ runs/<runId>/
37
+ run.json # run config, the attribution pins
38
+ events.jsonl # append-only event lines
39
+ artifacts/ # prediction CSVs, judge outputs, transcripts
40
+ ```
41
+
42
+ The set directory lives in the SAME git repository as the model it evaluates,
43
+ so a checkpoint (a git commit) pins the model and the ledger together. Never
44
+ place `evals/` inside the directory tree the answerer's package serves: gold
45
+ in the served tree is a contamination path.
46
+
47
+ Rules that make the ledger trustworthy:
48
+
49
+ - `events.jsonl` is append-only. Never edit a line. To void one, append a new
50
+ event whose payload marks the old one `voided`. One sanctioned exception: a
51
+ re-runnable stage (diagnose, improve) replaces ITS OWN prior events, keyed by
52
+ issue id, through `ledger.replace_events()` -- so a crash-and-rerun does not
53
+ duplicate issues. A stage never touches another stage's lines.
54
+ - A stage never rewrites another stage's fields.
55
+ - One artifact directory per attempt (`qid` plus `sample`). Never overwrite a
56
+ previous attempt's prediction CSV.
57
+ - End-of-run numbers come from counting event lines (`jq` over
58
+ `events.jsonl`), never from arithmetic recalled in prose.
59
+
60
+ ## `set.json`
61
+
62
+ | Field | Notes |
63
+ |---|---|
64
+ | `name` | Set name; also the directory name. |
65
+ | `description` | |
66
+ | `datasetVersion` | Integer. Bump on any golden repair or case change. Runs record the version they scored against. |
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. |
69
+ | `truthModel` | Model file inside that package. Default `truth.malloy`. |
70
+ | `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
+ ## `cases.jsonl`
73
+
74
+ One JSON object per line:
75
+
76
+ | Field | Notes |
77
+ |---|---|
78
+ | `qid` | Stable case id. |
79
+ | `question` | Exact text the answerer will see. |
80
+ | `split` | `dev` or `holdout`. Frozen at import. Diagnose and improve read `dev` only; the acceptance check runs both. |
81
+ | `tags` | list |
82
+ | `state` | `candidate` / `selected` / `excluded`. |
83
+ | `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.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
+ | `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
+ | `goldenRevision` | Integer. Bump on every golden change; `score` events stamp the revision they compared against. |
88
+ | `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 diagnosis to "retrieval ranking" for a failure that was never retrieval's. `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`. |
89
+
90
+ ## Term files (`eval-retrieval` only)
91
+
92
+ A customer set has no term file. The engine-side `eval-retrieval` skill keeps
93
+ `intents.jsonl` fixtures -- search term, entity type, a description of what the
94
+ searcher meant, a validity flag -- beside its own scripts, for evaluating the
95
+ engine against fixed inputs. Their shape and the `retrieval_score` events they
96
+ produce are documented there and validated here.
97
+
98
+ ## `run.json`
99
+
100
+ The attribution pins. Two runs are comparable only when these match where it
101
+ matters. Two runs with different `target` values are comparable on answer
102
+ verdicts only if the same model version reached both, which is rarely worth
103
+ assuming:
104
+
105
+ Required fields are the run's **identity** -- every run ever written carries
106
+ them. The rest are optional in the schema, but the **comparison pins** are
107
+ warned about when absent, because a run missing one cannot take part in an A/B
108
+ on that axis.
109
+
110
+ | Field | Notes |
111
+ |---|---|
112
+ | `runId` *(required)* | Directory name. |
113
+ | `target` *(required)* | Which server answered, and how it reached the data: `local`, `local-proxied`, or `platform`. Decides what an improve step is even allowed to do (see `skill:eval-loop`). |
114
+ | `answererModel` *(required)* | |
115
+ | `phase` / `started` *(required)* | |
116
+ | `judgeModel` / `judgeVersion` / `rubricSha` *(pins)* | The judge's model, and the version + content sha of `skill:eval-judge`. |
117
+ | `datasetVersion` *(pin)* | From `set.json` at run time. |
118
+ | `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. |
119
+ | `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
+ | `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
+ | `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. |
123
+ | `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. |
125
+ | `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). |
127
+ | `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
+ | `effort` | |
129
+ | `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. |
131
+ | `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
+ | `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
+ | `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. |
136
+
137
+ ## Events
138
+
139
+ Every line in `events.jsonl` is **flat**: `{ "kind": ..., <fields> }`, one
140
+ JSON object per line, with an optional `at` ISO timestamp. Case-scoped kinds
141
+ (`attempt`, `tool_call`, `score`) carry `qid`, `sample`, `phase` on the line;
142
+ run-level kinds do not. (An earlier draft of this document nested fields under
143
+ a `payload` with a `caseId`; no writer ever did that, and 23 run directories
144
+ exist in the flat shape, so the flat shape is the contract.) `kind` is one of:
145
+ `attempt`, `tool_call`, `score`, `retrieval_score`, `issue`, `issue_status`,
146
+ `candidate`, `acceptance_check`, `checkpoint`.
147
+
148
+ ### `attempt`
149
+
150
+ | Field | Type | Notes |
151
+ |---|---|---|
152
+ | `qid` | string | |
153
+ | `sample` | int or null | Which repeat. Required even when null. |
154
+ | `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. |
156
+ | `submitted` | bool | False when there was no final query. Not a wrong answer. |
157
+ | `final_query` | string or null | Required to replay. |
158
+ | `servedRevision` | string or null | From the package actually queried. |
159
+ | `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. |
162
+ | `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
+ | `contamination_reasons` | list | Empty when clean. |
164
+ | `input_tokens` / `output_tokens` / `cache_read_tokens` | int or null | Answerer token usage. Null when the host does not report it. |
165
+ | `cost_usd` | float or null | Answerer cost for this attempt. |
166
+ | `num_turns` / `wall_seconds` | int, float or null | |
167
+ | `answer_text` | string or null | The answer the judge scored. Kept so a verdict can be re-read without the transcript. |
168
+ | `transcriptPath` | string | The answerer's transcript under `artifacts/`. |
169
+
170
+ Token counts sit here rather than being derived later because the claim a
171
+ semantic model makes is about cost as well as correctness -- that a documented
172
+ model reaches a good answer in fewer turns and tokens than working from raw
173
+ schema. A ledger that counts calls but not tokens can state half of that.
174
+
175
+ ### `tool_call`
176
+
177
+ One event per MCP `get_context` or `execute_query` the attempt made.
178
+
179
+ | Field | Type | Notes |
180
+ |---|---|---|
181
+ | `tool` | string | `get_context` or `execute_query`. |
182
+ | `traceId` | string or null | `get_context` only; look up in your host's trace store. |
183
+ | `targets` | string, list or null | **What the answerer asked for**: the search terms it sent to `get_context`. Null for `execute_query`. |
184
+ | `rankedSummary` | object | Copied at capture from the trace so evidence survives trace eviction: `entityIds`, `ranks`, `resultCount`, and per-target `targets` with within-target ranks. |
185
+ | `error` | string or null | |
186
+
187
+ Never persist `execute_query` result rows, givens, or credentials.
188
+
189
+ `targets` records the request; `rankedSummary` records the response. Without
190
+ both, a low per-attempt recall has two readings that cannot be told apart: the
191
+ answerer searched for the wrong thing, or it searched well and retrieval ranked
192
+ the right entity too low. Those have opposite owners -- `agent-skill` and
193
+ `retrieval` -- so a ledger holding only the response cannot attribute the
194
+ failure, and any recall computed from it is a blend of answerer behaviour and
195
+ retrieval quality.
196
+
197
+ That blend is also why per-attempt recall is **not** comparable across arms: a
198
+ stronger answerer searches better and scores higher retrieval recall without
199
+ retrieval having changed. Use it for attribution within an arm. Comparing
200
+ retrieval itself between engine versions is `eval-retrieval`'s job, with fixed
201
+ terms, and is not something a customer run reports.
202
+
203
+ ### `score`
204
+
205
+ The answer judge's verdict for one attempt (protocol in
206
+ `skill:eval-judge`). Every attempt in a scored run gets exactly one.
207
+
208
+ | Field | Type | Notes |
209
+ |---|---|---|
210
+ | `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`). |
212
+ | `confidence` | int or null | 1 to 10. Confidence of 5 or lower forces `needs_human`. |
213
+ | `column_pairing` | object or null | The judge's named gold-to-prediction column correspondence. |
214
+ | `judge_version` / `rubric_sha` | string | Pins which rubric produced this verdict. |
215
+ | `golden_revision` | int | From the case at score time. |
216
+ | `contaminated` | bool or `"unknown"` | Copied from the attempt; true or unknown means `verdict: null`. |
217
+ | `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. |
219
+ | `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
+
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.
224
+
225
+ Aggregates count decided verdicts only. `match` and `no_match` are the pass and
226
+ the fail; **`near_match`, `needs_human` and null are none of the above** and stay
227
+ out of acceptance arithmetic. `near_match` is excluded because it means "defensibly
228
+ different", and an arguable verdict that moves a pass rate is a measurement
229
+ artefact rather than a result (`skill:eval-judge` rule 7). Report its count:
230
+ it rising is how a set tells you its rubrics are going vague.
231
+
232
+ This applies to `score`. On `retrieval_score` below, `near_match` **is** counted
233
+ towards recall and precision, because an overlapping entity is a genuine
234
+ retrieval success. The two are different questions that share a word.
235
+
236
+ ### `retrieval_score` (written by `eval-retrieval`, not by a customer run)
237
+
238
+ One per term judged in a fixed-term replay (run-level; no `caseId`).
239
+
240
+ | Field | Type | Notes |
241
+ |---|---|---|
242
+ | `intentId` / `term` / `entityType` | string | From the term file. |
243
+ | `in_scope` | bool | Does THIS model version represent the concept? False is a coverage gap, not a retrieval failure. |
244
+ | `judgments` | list | Per returned entity: `entityId`, `rank`, `level` (`match` / `near_match` / `no_match`), `confidence`, `why`. Empty when nothing returned. |
245
+ | `judge_version` / `rubric_sha` | string | |
246
+ | `traceId` | string or null | The `get_context` call judged. |
247
+
248
+ Run-level metrics fall out by counting:
249
+
250
+ - `coverage` = in-scope terms / valid terms -- a property of the model and its
251
+ data, reported on its own, never as a retrieval number.
252
+ - `recall` (on in-scope terms) = fraction whose judgments contain a `match`.
253
+ - `precision@N` = `match` judgments / judged, with N stated.
254
+
255
+ ### `issue`
256
+
257
+ | Field | Type | Notes |
258
+ |---|---|---|
259
+ | `issue_id` | string | Stable across status events. |
260
+ | `qids` | list | Affected cases. |
261
+ | `primary_code` / `contributing_codes` | string / list | From `skill:eval-diagnose`, verbatim. |
262
+ | `component` | string | `dataset` / `agent-call` / `get_context/model` / `get_context/retrieval` / `construction` / `model-definition`. |
263
+ | `owner` | string | `model` / `retrieval` / `agent-skill` / `dataset`. Environment failures stop the run; they are never diagnosed, so there is no environment owner. |
264
+ | `severity` / `confidence` | string | |
265
+ | `sufficiency` | string | `sufficient` / `insufficient` / `unknown`. |
266
+ | `traceIds` | list | |
267
+ | `diagnosis` | string | Written before any edit exists. |
268
+
269
+ ### `issue_status`
270
+
271
+ `issue_id` plus `status`: `open` / `batched` / `fixed` / `rejected` /
272
+ `deferred`. Readers take the latest event for that `issue_id`. Do not invent
273
+ a status column.
274
+
275
+ ### `candidate`
276
+
277
+ Written by `eval-improve`, for every proposed edit, accepted or not. A
278
+ rejected direction keeps its record.
279
+
280
+ | Field | Notes |
281
+ |---|---|
282
+ | `issue_ids` | |
283
+ | `files` | Paths the edit touched. |
284
+ | `diffSummary` | One line per file. |
285
+ | `probes` | Query and result for each factual claim. |
286
+ | `meaningChanged` | Entities whose *meaning* the edit changed; `[]` for a docs-only edit. |
287
+ | `goldenSuspect` | Each `{qid, entity, stored, rederived}`: a golden this edit may have invalidated. Reported by the improver, never repaired by it. **Non-empty halts the acceptance check** until adjudicated through the golden side door. |
288
+ | `goldenAudit` | The set's `verify_goldens.py` run against the edited model: `{ran, clean, model, tail}`. Catches drift and rubric-vs-model contradictions only; a golden whose value silently moved is invisible to it, which is why `goldenSuspect` exists alongside. |
289
+
290
+ ### `acceptance_check`
291
+
292
+ Called `gate` before 2026-09-03. `ledger.py` reads the old kind as this one, so
293
+ run directories written earlier still validate and nothing rewrites them.
294
+
295
+ Written by `eval-loop`, one per acceptance check decision, BEFORE any checkpoint commit.
296
+
297
+ | Field | Notes |
298
+ |---|---|
299
+ | `issue_ids` | |
300
+ | `decision` | `accepted` / `rejected`. |
301
+ | `class` | `docs` / `definition` / `retrieval` / `justified`. |
302
+ | `baselineRunId` / `finalRunIds` | Plural: acceptance needs two independent runs. |
303
+ | `regressions` | Case ids whose verdict got worse vs baseline. Must be empty to accept. |
304
+ | `holdoutDelta` | Confident-verdict delta on the holdout slice. |
305
+ | `reason` | Including independent deterministic justification when that is the basis. |
306
+
307
+ ### `checkpoint`
308
+
309
+ Written by `eval-loop` after an accepted acceptance check, or when a restore runs. The
310
+ model bytes live in git, not in this payload.
311
+
312
+ | Field | Notes |
313
+ |---|---|
314
+ | `action` | `created` / `restored`. |
315
+ | `label` | |
316
+ | `modelGitSha` | The commit this checkpoint names (create), or the commit restored to. |
317
+ | `issueIds` | Issues the accepted edit closed. Empty on restore. |
318
+
319
+ ## `judge-regressions.jsonl`
320
+
321
+ Append a line whenever a human overrules a judge verdict: the case or intent,
322
+ the judge's verdict, the human's, and why. Re-run this file against the judge
323
+ whenever `skill:eval-judge` or the judge model changes; a rubric change that
324
+ flips old human-settled verdicts is a judge regression, not new truth.