skillfid 0.1.0
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/LICENSE +21 -0
- package/README.md +303 -0
- package/package.json +39 -0
- package/src/baseline.js +85 -0
- package/src/calibration.js +32 -0
- package/src/cli.js +329 -0
- package/src/concurrency.js +77 -0
- package/src/copilot-sdk.js +137 -0
- package/src/corpus.js +112 -0
- package/src/dataset.js +486 -0
- package/src/diagnosis.js +40 -0
- package/src/evaluation.js +486 -0
- package/src/files.js +56 -0
- package/src/inventory.js +83 -0
- package/src/journal.js +193 -0
- package/src/json.js +26 -0
- package/src/judge.js +39 -0
- package/src/progress.js +223 -0
- package/src/prompts.js +42 -0
- package/src/questions.js +80 -0
- package/src/report.js +233 -0
- package/src/structured.js +35 -0
- package/src/verification.js +33 -0
- package/templates/evaluation-report.html +537 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Waldek Mastykarz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# skillfid
|
|
2
|
+
|
|
3
|
+
`skillfid` is a CLI for developers who build agent skills from documentation. It tests whether a skill helps an agent answer questions grounded in that source, revealing missing or imprecise guidance that reviewing the skill alone can miss. Use it to compare skill revisions against a reusable closed-book baseline and identify source-backed improvements.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
## Explore the Canopy result
|
|
8
|
+
|
|
9
|
+
> Canopy, is a synthetic distributed build cache used to demonstrate the complete workflow.
|
|
10
|
+
|
|
11
|
+
Inspect the included evaluation and trace its findings to the recorded data without
|
|
12
|
+
making a model call:
|
|
13
|
+
|
|
14
|
+
- [Open the self-contained HTML report](examples/canopy/runs/run_1689576d3f9343ce/report.html)
|
|
15
|
+
- Inspect the run's [summary](examples/canopy/runs/run_1689576d3f9343ce/summary.json), [diagnoses](examples/canopy/runs/run_1689576d3f9343ce/diagnoses.jsonl), and [recorded answers](examples/canopy/runs/run_1689576d3f9343ce/answers.jsonl)
|
|
16
|
+
- Trace findings through the [generated questions](examples/canopy/dataset/ds_7f1bc4dc92fc4b51/questions.jsonl), [evidence](examples/canopy/dataset/ds_7f1bc4dc92fc4b51/evidence.jsonl), and [source corpus](examples/canopy/corpus/cache-operations.md)
|
|
17
|
+
- Compare the intentionally compressed [original skill](examples/canopy/skill/SKILL.md) with the source-faithful [v2 skill](examples/canopy/skill-v2/SKILL.md)
|
|
18
|
+
|
|
19
|
+
The original skill scored **83.91%**, compared with **0%** closed book. After
|
|
20
|
+
source-backed improvements, v2 scored **100%** against the same dataset and
|
|
21
|
+
baseline. Both results used `gpt-5.6-sol` as subject and judge with three trials per
|
|
22
|
+
question. Scores are specific to the dataset and evaluation configuration.
|
|
23
|
+
|
|
24
|
+
## Run the complete Canopy flow
|
|
25
|
+
|
|
26
|
+
Complete the [requirements](#requirements) and [installation](#installation) first.
|
|
27
|
+
Then build an immutable dataset from the Canopy corpus:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
skillfid --progress human dataset build \
|
|
31
|
+
--corpus ./examples/canopy/corpus \
|
|
32
|
+
--output-dir ./.work/canopy/datasets \
|
|
33
|
+
--work-dir ./.work/canopy/dataset-work
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Set `DATASET` to the published path printed by the command, then verify it locally:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
DATASET=./.work/canopy/datasets/<dataset-id>
|
|
40
|
+
|
|
41
|
+
skillfid dataset verify --dataset "$DATASET"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Create the reusable closed-book baseline:
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
skillfid --progress human eval baseline \
|
|
48
|
+
--dataset "$DATASET" \
|
|
49
|
+
--output-dir ./.work/canopy/baselines \
|
|
50
|
+
--work-dir ./.work/canopy/baseline-work
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Evaluate the original skill with explicit invocation. The run reuses the compatible
|
|
54
|
+
baseline automatically:
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
skillfid --progress human eval run \
|
|
58
|
+
--dataset "$DATASET" \
|
|
59
|
+
--skill ./examples/canopy/skill \
|
|
60
|
+
--baseline-dir ./.work/canopy/baselines \
|
|
61
|
+
--skill-invocation explicit \
|
|
62
|
+
--output-dir ./.work/canopy/runs \
|
|
63
|
+
--work-dir ./.work/canopy/eval-work
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Set `RUN` to the output path, then generate the report locally:
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
RUN=./.work/canopy/runs/<run-id>
|
|
70
|
+
|
|
71
|
+
skillfid eval report \
|
|
72
|
+
--run "$RUN" \
|
|
73
|
+
--dataset "$DATASET" \
|
|
74
|
+
--title "Canopy cache operations"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
To evaluate the improved skill, rerun `eval run` with
|
|
78
|
+
`--skill ./examples/canopy/skill-v2`. See the
|
|
79
|
+
[Canopy walkthrough](examples/canopy/README.md) for the checked-in artifacts and
|
|
80
|
+
reproduction paths. If a model-backed command is interrupted, use the resume command
|
|
81
|
+
printed by the CLI.
|
|
82
|
+
|
|
83
|
+
## Requirements
|
|
84
|
+
|
|
85
|
+
Before running `skillfid`, make sure that you have:
|
|
86
|
+
|
|
87
|
+
- Node.js 24 or later
|
|
88
|
+
- GitHub Copilot CLI authenticated for Copilot access
|
|
89
|
+
|
|
90
|
+
Check them with `node --version` and `copilot --version`. The default model for both
|
|
91
|
+
subject and judge is `gpt-5.6-sol`; you can override either per command. Evaluation
|
|
92
|
+
runs use an isolated repository-local profile and do not expose authentication
|
|
93
|
+
tokens to agent tools.
|
|
94
|
+
|
|
95
|
+
## Installation
|
|
96
|
+
|
|
97
|
+
Install `skillfid` from npm:
|
|
98
|
+
|
|
99
|
+
```sh
|
|
100
|
+
npm install --global skillfid
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
For a minimal model-backed smoke test, use the one-fact
|
|
104
|
+
[Arbor example](examples/arbor/README.md).
|
|
105
|
+
|
|
106
|
+
## Command reference
|
|
107
|
+
|
|
108
|
+
Build an immutable, oracle-calibrated dataset from a Markdown corpus:
|
|
109
|
+
|
|
110
|
+
```sh
|
|
111
|
+
skillfid dataset build --corpus ./corpus --json
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Every generated question must produce an oracle answer that receives a stable,
|
|
115
|
+
perfect criterion-level judgment before publication. The same answer is judged
|
|
116
|
+
independently three times. Unanimous results are accepted; disagreement triggers
|
|
117
|
+
two more judgments, and only a 4/5 result is accepted. A 3/2 split is unstable and
|
|
118
|
+
blocks publication.
|
|
119
|
+
|
|
120
|
+
The dataset stores every judgment and its consensus in `calibrations.jsonl`; the
|
|
121
|
+
corpus remains the sole ground truth. Datasets older than schema v6 lack the combined
|
|
122
|
+
structural and integrity proof and must be rebuilt.
|
|
123
|
+
|
|
124
|
+
After changing the Copilot runtime, model, judge, or harness, recalibrate without
|
|
125
|
+
repeating corpus inventory or question extraction:
|
|
126
|
+
|
|
127
|
+
```sh
|
|
128
|
+
skillfid dataset recalibrate \
|
|
129
|
+
--dataset ./datasets/<dataset-id> \
|
|
130
|
+
--output-dir ./datasets \
|
|
131
|
+
--json
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Recalibration copies documents, knowledge, evidence, questions, verification,
|
|
135
|
+
coverage, and audit records unchanged. It reruns only the oracle answer and
|
|
136
|
+
independent consensus judgments for each question. The oracle answer is generated
|
|
137
|
+
once and held fixed across all judge repeats. Every question must still receive a
|
|
138
|
+
stable, perfect calibration before publication.
|
|
139
|
+
|
|
140
|
+
The result is a new immutable dataset whose manifest records `sourceDatasetId`.
|
|
141
|
+
Continue interrupted recalibration with the same command and `--resume`.
|
|
142
|
+
|
|
143
|
+
Verify a dataset locally without rerunning extraction:
|
|
144
|
+
|
|
145
|
+
```sh
|
|
146
|
+
skillfid dataset verify --dataset ./datasets/<dataset-id> --json
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Measure the reusable closed-book baseline:
|
|
150
|
+
|
|
151
|
+
```sh
|
|
152
|
+
skillfid eval baseline \
|
|
153
|
+
--dataset ./datasets/<dataset-id> \
|
|
154
|
+
--json
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Evaluate a skill using the latest exactly compatible baseline:
|
|
158
|
+
|
|
159
|
+
```sh
|
|
160
|
+
skillfid eval run \
|
|
161
|
+
--dataset ./datasets/<dataset-id> \
|
|
162
|
+
--skill ./path/to/skill \
|
|
163
|
+
--json
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Skill activation is automatic by default. To invoke the discovered project skill as
|
|
167
|
+
`/skill-name` in every skill-condition prompt, use explicit invocation:
|
|
168
|
+
|
|
169
|
+
```sh
|
|
170
|
+
skillfid eval run \
|
|
171
|
+
--dataset ./datasets/<dataset-id> \
|
|
172
|
+
--skill ./path/to/skill \
|
|
173
|
+
--skill-invocation explicit \
|
|
174
|
+
--json
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`--skill-invocation` accepts `auto` or `explicit`. It changes only the skill
|
|
178
|
+
condition. The resolved mode is recorded in the run manifest; the baseline does not
|
|
179
|
+
change.
|
|
180
|
+
|
|
181
|
+
Oracle calibration is a dataset publication gate, not an evaluation condition or
|
|
182
|
+
model-specific ceiling. Baseline and skill model settings inherit from the dataset
|
|
183
|
+
by default, but both commands may select another model configuration.
|
|
184
|
+
|
|
185
|
+
A compatible baseline must match the dataset ID, subject model, judge model,
|
|
186
|
+
reasoning effort, trial count, evaluator version, and Copilot CLI version exactly.
|
|
187
|
+
Evaluation fails with an actionable message when none exists.
|
|
188
|
+
|
|
189
|
+
Both commands default to three trials per question. Use `--trials <count>`
|
|
190
|
+
consistently to override that default. When storing baselines outside
|
|
191
|
+
`./baselines`, use matching `--output-dir` on `eval baseline` and `--baseline-dir`
|
|
192
|
+
on `eval run`.
|
|
193
|
+
|
|
194
|
+
Generate a self-contained HTML report:
|
|
195
|
+
|
|
196
|
+
```sh
|
|
197
|
+
skillfid eval report \
|
|
198
|
+
--run ./runs/<run-id> \
|
|
199
|
+
--dataset ./datasets/<dataset-id> \
|
|
200
|
+
--title "My skill"
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The report defaults to `<run>/report.html`. Use `--output <file>` to choose another
|
|
204
|
+
location. Report generation is local and makes no Copilot calls.
|
|
205
|
+
|
|
206
|
+
## Execution and recovery
|
|
207
|
+
|
|
208
|
+
Run `skillfid --help` for the complete command reference, including JSON
|
|
209
|
+
schemas, prerequisites, and exit codes. Primary output goes to stdout, while
|
|
210
|
+
progress and errors go to stderr.
|
|
211
|
+
|
|
212
|
+
Copilot calls have a 600-second timeout and one fresh-session retry by default.
|
|
213
|
+
Configure them with `--timeout <seconds>` and `--timeout-retries <count>`.
|
|
214
|
+
|
|
215
|
+
Dataset builds and evaluations run independent work concurrently. The default
|
|
216
|
+
is 10; set `--concurrency <count>` to any positive integer. Answers and judgments
|
|
217
|
+
are separate recovery checkpoints, so interruption after answering does not
|
|
218
|
+
require generating that answer again.
|
|
219
|
+
|
|
220
|
+
Matching operations reuse completed work by default. Resume selects the latest
|
|
221
|
+
matching incomplete operation, including one originally started with `--fresh`.
|
|
222
|
+
Validated jobs are stored in
|
|
223
|
+
`<work-dir>/operations.sqlite` using SQLite WAL and retained after completion.
|
|
224
|
+
|
|
225
|
+
Use `--fresh` to start from scratch without deleting earlier state. When an
|
|
226
|
+
interactive operation is interrupted, the CLI prints the exact resume command, so
|
|
227
|
+
you do not need to reconstruct it.
|
|
228
|
+
|
|
229
|
+
Inspect retained operations without modifying the journal:
|
|
230
|
+
|
|
231
|
+
```sh
|
|
232
|
+
skillfid operation status --work-dir .work/eval --json
|
|
233
|
+
skillfid operation status --work-dir .work/eval --operation-id <id> --json
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
All commands support `--progress auto|human|agent|json|quiet`. Auto selects an
|
|
237
|
+
in-place display on a TTY and bounded agent snapshots otherwise. Human mode shows
|
|
238
|
+
current work and elapsed time, followed by an estimate and a compact result. JSON
|
|
239
|
+
progress is emitted as JSON Lines on stderr without changing final stdout.
|
|
240
|
+
|
|
241
|
+
## Artifacts and scoring
|
|
242
|
+
|
|
243
|
+
Each baseline stores its closed-book answers and judgments with an exact
|
|
244
|
+
compatibility manifest. Each skill run embeds those baseline records alongside
|
|
245
|
+
fresh skill answers. You will find the answers in `answers.jsonl`, criterion-level
|
|
246
|
+
judgments in `judgments.jsonl`, failure analysis in `diagnoses.jsonl`, and aggregate
|
|
247
|
+
scores in `summary.json`.
|
|
248
|
+
|
|
249
|
+
Embedding baseline records keeps reports self-contained without repeating closed-book
|
|
250
|
+
inference. The summary retains build-time calibration metadata for compatibility;
|
|
251
|
+
calibration is not an evaluation condition or model call.
|
|
252
|
+
|
|
253
|
+
HTML reports preserve that separation. Scores come from all recorded trials, and
|
|
254
|
+
uplift is shown in percentage points. Only diagnoses with concrete file targets
|
|
255
|
+
appear as recommended work; the evidence view retains every trial answer and its
|
|
256
|
+
failed-criterion rationale.
|
|
257
|
+
|
|
258
|
+
## Isolation and safety
|
|
259
|
+
|
|
260
|
+
Every question, trial, and condition runs in a fresh non-resumed Copilot SDK session
|
|
261
|
+
with its own filesystem workspace. Subject sessions share one client, and judge
|
|
262
|
+
sessions share another. Conversation and workspace state are not reused.
|
|
263
|
+
|
|
264
|
+
Evaluation calls deny shell execution, file writes, and URL access. This stops the
|
|
265
|
+
closed-book baseline from searching external sources while preserving local read
|
|
266
|
+
access for skill files. Service, authentication, and validation failures surface
|
|
267
|
+
immediately while completed journal checkpoints remain resumable.
|
|
268
|
+
|
|
269
|
+
See [DESIGN.md](DESIGN.md) for the architecture, scheduler behavior, and scoring
|
|
270
|
+
model.
|
|
271
|
+
|
|
272
|
+
## Development
|
|
273
|
+
|
|
274
|
+
Run the tests:
|
|
275
|
+
|
|
276
|
+
```sh
|
|
277
|
+
npm test
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Ferryline is the synthetic distributed build-cache scenario from GitHub Next's
|
|
281
|
+
Knowledge Compressor article. Reproduce its calibration from a repository checkout;
|
|
282
|
+
the extraction script requires internet access to fetch the article:
|
|
283
|
+
|
|
284
|
+
```sh
|
|
285
|
+
npm run calibration:extract
|
|
286
|
+
npm start -- dataset build \
|
|
287
|
+
--corpus .work/js-calibration/article/corpus \
|
|
288
|
+
--output-dir .work/js-calibration/datasets \
|
|
289
|
+
--json
|
|
290
|
+
npm run calibration:compare -- \
|
|
291
|
+
.work/js-calibration/datasets/<dataset-id> \
|
|
292
|
+
.work/js-calibration/article/reference-questions.json
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## Support and contributing
|
|
296
|
+
|
|
297
|
+
Report defects and request features in
|
|
298
|
+
[GitHub Issues](https://github.com/waldekmastykarz/knowledge-eval/issues). Read
|
|
299
|
+
[CONTRIBUTING.md](CONTRIBUTING.md) before opening a pull request.
|
|
300
|
+
|
|
301
|
+
## License
|
|
302
|
+
|
|
303
|
+
Licensed under the [MIT License](LICENSE).
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "skillfid",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Evaluate how faithfully agent skills apply their source documentation",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"agent-skills",
|
|
7
|
+
"copilot",
|
|
8
|
+
"evaluation",
|
|
9
|
+
"grounding"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://github.com/waldekmastykarz/skillfid#readme",
|
|
12
|
+
"bugs": "https://github.com/waldekmastykarz/skillfid/issues",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/waldekmastykarz/skillfid.git"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": "Waldek Mastykarz <waldek@mastykarz.nl>",
|
|
19
|
+
"type": "module",
|
|
20
|
+
"files": [
|
|
21
|
+
"src/",
|
|
22
|
+
"templates/evaluation-report.html"
|
|
23
|
+
],
|
|
24
|
+
"bin": {
|
|
25
|
+
"skillfid": "./src/cli.js"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"calibration:extract": "node scripts/extract-ferryline.js",
|
|
29
|
+
"calibration:compare": "node scripts/compare-questions.js",
|
|
30
|
+
"test": "node --test",
|
|
31
|
+
"start": "node src/cli.js"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=24"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@github/copilot-sdk": "^1.0.11"
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/baseline.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, readdir } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { readJson, readJsonl, writeJson, writeJsonl } from './files.js';
|
|
6
|
+
import { stableStringify } from './json.js';
|
|
7
|
+
|
|
8
|
+
export class BaselineError extends Error {}
|
|
9
|
+
|
|
10
|
+
export function baselineCompatibilityKey(compatibility) {
|
|
11
|
+
return createHash('sha256').update(stableStringify(compatibility), 'utf8').digest('hex');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function writeBaseline({ outputRoot, compatibility, answers, judgments }) {
|
|
15
|
+
assertBaselineRecords(answers, judgments);
|
|
16
|
+
const baselineId = `baseline_${randomUUID().replaceAll('-', '').slice(0, 16)}`;
|
|
17
|
+
const root = path.resolve(outputRoot);
|
|
18
|
+
await mkdir(root, { recursive: true });
|
|
19
|
+
const destination = path.join(root, baselineId);
|
|
20
|
+
await mkdir(destination, { recursive: false });
|
|
21
|
+
await writeJsonl(path.join(destination, 'answers.jsonl'), answers);
|
|
22
|
+
await writeJsonl(path.join(destination, 'judgments.jsonl'), judgments);
|
|
23
|
+
await writeJson(path.join(destination, 'manifest.json'), {
|
|
24
|
+
baselineId,
|
|
25
|
+
compatibilityKey: baselineCompatibilityKey(compatibility),
|
|
26
|
+
createdAt: new Date().toISOString(),
|
|
27
|
+
...compatibility,
|
|
28
|
+
});
|
|
29
|
+
return destination;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function findCompatibleBaseline({ outputRoot, compatibility }) {
|
|
33
|
+
const root = path.resolve(outputRoot);
|
|
34
|
+
let entries;
|
|
35
|
+
try { entries = await readdir(root, { withFileTypes: true }); }
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (error.code === 'ENOENT') return undefined;
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
const compatibilityKey = baselineCompatibilityKey(compatibility);
|
|
41
|
+
const candidates = [];
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
if (!entry.isDirectory() || !entry.name.startsWith('baseline_')) continue;
|
|
44
|
+
const baselinePath = path.join(root, entry.name);
|
|
45
|
+
const manifest = await readJson(path.join(baselinePath, 'manifest.json'));
|
|
46
|
+
if (manifest.compatibilityKey === compatibilityKey) candidates.push({ baselinePath, manifest });
|
|
47
|
+
}
|
|
48
|
+
candidates.sort((left, right) => right.manifest.createdAt.localeCompare(left.manifest.createdAt) || right.manifest.baselineId.localeCompare(left.manifest.baselineId));
|
|
49
|
+
if (!candidates.length) return undefined;
|
|
50
|
+
return loadBaseline(candidates[0].baselinePath, compatibility);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function loadBaseline(baselinePath, compatibility) {
|
|
54
|
+
const root = path.resolve(baselinePath);
|
|
55
|
+
const [manifest, answers, judgments] = await Promise.all([
|
|
56
|
+
readJson(path.join(root, 'manifest.json')),
|
|
57
|
+
readJsonl(path.join(root, 'answers.jsonl')),
|
|
58
|
+
readJsonl(path.join(root, 'judgments.jsonl')),
|
|
59
|
+
]);
|
|
60
|
+
const expectedKey = baselineCompatibilityKey(compatibility);
|
|
61
|
+
if (manifest.compatibilityKey !== expectedKey) throw new BaselineError(`Baseline ${manifest.baselineId ?? root} is not compatible with this evaluation configuration`);
|
|
62
|
+
assertBaselineRecords(answers, judgments);
|
|
63
|
+
return { baselinePath: root, manifest, answers, judgments };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function assertBaselineCoverage({ answers, judgments }, questions, trialsPerQuestion) {
|
|
67
|
+
assertBaselineRecords(answers, judgments);
|
|
68
|
+
const expectedKeys = new Set(questions.flatMap((question) => Array.from({ length: trialsPerQuestion }, (_, trial) => `${question.testId}:${trial}`)));
|
|
69
|
+
const answerKeys = new Set(answers.map(recordKey));
|
|
70
|
+
const judgmentKeys = new Set(judgments.map(recordKey));
|
|
71
|
+
if (answerKeys.size !== expectedKeys.size || [...expectedKeys].some((key) => !answerKeys.has(key) || !judgmentKeys.has(key))) {
|
|
72
|
+
throw new BaselineError(`Baseline coverage does not match ${questions.length} questions with ${trialsPerQuestion} trials each`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function assertBaselineRecords(answers, judgments) {
|
|
77
|
+
if (!answers.length || answers.some((answer) => answer.condition !== 'closedBook')) throw new BaselineError('Baseline answers must contain only closed-book records');
|
|
78
|
+
if (judgments.length !== answers.length || judgments.some((judgment) => judgment.condition !== 'closedBook')) throw new BaselineError('Baseline judgments must match the closed-book answers');
|
|
79
|
+
const answerKeys = new Set(answers.map(recordKey));
|
|
80
|
+
if (answerKeys.size !== answers.length || judgments.some((judgment) => !answerKeys.has(recordKey(judgment)))) throw new BaselineError('Baseline answers and judgments must have matching unique test and trial records');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function recordKey(record) {
|
|
84
|
+
return `${record.testId}:${record.trial}`;
|
|
85
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { stableStringify } from './json.js';
|
|
2
|
+
import { InvalidStructuredResponse, parseJsonObject } from './structured.js';
|
|
3
|
+
import { parseJudgeResponse } from './judge.js';
|
|
4
|
+
|
|
5
|
+
const INTEGRITY_FIELDS = ['directlyEntailed', 'contradictionChecked', 'qualificationsIncluded', 'authorityResolved', 'proxyAnswer'];
|
|
6
|
+
const VERIFICATION_FIELDS = ['answerable', 'referenceSupported', 'criteriaSupported', 'mappedItemsRequired', 'requiresCorpusKnowledge', 'unambiguous', 'answerLeakage'];
|
|
7
|
+
|
|
8
|
+
export function buildCalibrationPrompt({ question, source, candidateAnswer, rubric }) {
|
|
9
|
+
return [
|
|
10
|
+
'Calibrate this question and oracle answer against the complete supplied source documents. Return only valid JSON with this shape:',
|
|
11
|
+
'{"criterionResults":[{"criterionIndex":0,"score":1,"rationale":"..."}],"unsupportedClaims":[],"verification":{"answerable":true,"referenceSupported":true,"criteriaSupported":true,"mappedItemsRequired":true,"requiresCorpusKnowledge":true,"unambiguous":true,"answerLeakage":false,"reason":"..."},"integrity":{"directlyEntailed":true,"contradictionChecked":true,"qualificationsIncluded":true,"authorityResolved":true,"proxyAnswer":false,"rationale":"..."}}',
|
|
12
|
+
'Score every rubric criterion from 0 to 1. directlyEntailed means every answer claim follows from the complete source. contradictionChecked means the complete source was checked for conflicting passages. qualificationsIncluded means applicable exceptions and prerequisites are represented. authorityResolved means version and authority precedence are correctly handled. proxyAnswer is true when the answer merely resembles local evidence without establishing the source-supported answer. Set each field independently and explain the integrity verdict.',
|
|
13
|
+
'INPUT:',
|
|
14
|
+
stableStringify({ question, source, candidateAnswer, rubric }),
|
|
15
|
+
].join('\n');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function parseCalibrationResponse(response, criterionCount) {
|
|
19
|
+
const data = parseJsonObject(response, 'Calibration response');
|
|
20
|
+
const judgment = parseJudgeResponse(response, criterionCount);
|
|
21
|
+
if (data.verification === null || Array.isArray(data.verification) || typeof data.verification !== 'object') throw new InvalidStructuredResponse('verification must be an object');
|
|
22
|
+
for (const field of VERIFICATION_FIELDS) if (typeof data.verification[field] !== 'boolean') throw new InvalidStructuredResponse(`verification.${field} must be a boolean`);
|
|
23
|
+
if (typeof data.verification.reason !== 'string' || !data.verification.reason.trim()) throw new InvalidStructuredResponse('verification.reason must be a non-empty string');
|
|
24
|
+
const verification = { ...Object.fromEntries(VERIFICATION_FIELDS.map((field) => [field, data.verification[field]])), reason: data.verification.reason.trim() };
|
|
25
|
+
verification.passed = verification.answerable && verification.referenceSupported && verification.criteriaSupported && verification.mappedItemsRequired && verification.requiresCorpusKnowledge && verification.unambiguous && !verification.answerLeakage;
|
|
26
|
+
if (data.integrity === null || Array.isArray(data.integrity) || typeof data.integrity !== 'object') throw new InvalidStructuredResponse('integrity must be an object');
|
|
27
|
+
for (const field of INTEGRITY_FIELDS) if (typeof data.integrity[field] !== 'boolean') throw new InvalidStructuredResponse(`integrity.${field} must be a boolean`);
|
|
28
|
+
if (typeof data.integrity.rationale !== 'string' || !data.integrity.rationale.trim()) throw new InvalidStructuredResponse('integrity.rationale must be a non-empty string');
|
|
29
|
+
const integrity = { ...Object.fromEntries(INTEGRITY_FIELDS.map((field) => [field, data.integrity[field]])), rationale: data.integrity.rationale.trim() };
|
|
30
|
+
integrity.passed = integrity.directlyEntailed && integrity.contradictionChecked && integrity.qualificationsIncluded && integrity.authorityResolved && !integrity.proxyAnswer;
|
|
31
|
+
return { ...judgment, verification, integrity };
|
|
32
|
+
}
|