@skill-harness/core 0.3.0 → 0.3.1
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/dist/lift.d.ts +23 -15
- package/dist/lift.js +43 -4
- package/package.json +34 -8
package/dist/lift.d.ts
CHANGED
|
@@ -37,10 +37,30 @@ export interface Lift {
|
|
|
37
37
|
/** Ids the green run covered that the red baseline did not (and vice versa). */
|
|
38
38
|
greenOnly: string[];
|
|
39
39
|
redOnly: string[];
|
|
40
|
+
/**
|
|
41
|
+
* Ids both runs covered that a lift cannot speak to, because the harness runs
|
|
42
|
+
* them identically in red and green. Reported rather than compared: folding them
|
|
43
|
+
* in would credit the red side with passes the skill itself produced. See
|
|
44
|
+
* `LiftOptions.modeInsensitive`.
|
|
45
|
+
*/
|
|
46
|
+
modeInsensitive: string[];
|
|
40
47
|
/** True when either side was an `--only` run, so coverage is a subset by construction. */
|
|
41
48
|
partial: boolean;
|
|
42
49
|
cells: Record<string, LiftCell>;
|
|
43
50
|
}
|
|
51
|
+
export interface LiftOptions {
|
|
52
|
+
/**
|
|
53
|
+
* Scenario ids whose red and green runs are the same run by construction, so
|
|
54
|
+
* comparing them measures nothing.
|
|
55
|
+
*
|
|
56
|
+
* The case that exists today is `system_prompt_file`: the pi adapter treats an
|
|
57
|
+
* agent-file scenario's file AS the system prompt and passes `--no-skills`
|
|
58
|
+
* *whatever the mode*, so the skill is loaded on both sides. Left in, such a
|
|
59
|
+
* cell lands in `kept` (or `both-fail`) and drags the denominator down —
|
|
60
|
+
* understating lift with evidence that the skill worked.
|
|
61
|
+
*/
|
|
62
|
+
modeInsensitive?: Iterable<string>;
|
|
63
|
+
}
|
|
44
64
|
/**
|
|
45
65
|
* Compare a red (baseline, skill off) run against a green (skill active) run of
|
|
46
66
|
* the same model: the "does this skill actually do anything?" measurement.
|
|
@@ -48,22 +68,10 @@ export interface Lift {
|
|
|
48
68
|
* Both sides go through `effectiveVerdicts`, so an author override is what
|
|
49
69
|
* counts and an override resolves a misfire — the same rule scoring uses. Only
|
|
50
70
|
* the intersection of scenario ids is compared; a lift cannot speak to a
|
|
51
|
-
* scenario one side never ran
|
|
71
|
+
* scenario one side never ran, nor to one the harness ran identically in both
|
|
72
|
+
* modes (`opts.modeInsensitive`).
|
|
52
73
|
*/
|
|
53
|
-
export declare function computeLift(red: ResultsFile, green: ResultsFile): Lift;
|
|
74
|
+
export declare function computeLift(red: ResultsFile, green: ResultsFile, opts?: LiftOptions): Lift;
|
|
54
75
|
/** One line for a human: what the skill did, and what it cost. */
|
|
55
76
|
export declare function liftHeadline(lift: Lift): string;
|
|
56
|
-
/**
|
|
57
|
-
* Per model-tag under <skillDir>/tests/results/, pair the most recent red run
|
|
58
|
-
* with the most recent green run and compute the lift.
|
|
59
|
-
*
|
|
60
|
-
* Deliberately derived on read rather than persisted into results.yaml: a lift
|
|
61
|
-
* is a fact about a *pair* of runs, so caching it inside one run's file would go
|
|
62
|
-
* stale the moment a new baseline lands — the stale-scorecard failure mode
|
|
63
|
-
* `source_hashes` exists to prevent. Deriving also means lift works
|
|
64
|
-
* retroactively on results already committed by 0.1.x/0.2.0 users.
|
|
65
|
-
*
|
|
66
|
-
* A tag with no red baseline is omitted entirely rather than reported as a zero
|
|
67
|
-
* lift: "not measured" and "measured no effect" are different claims.
|
|
68
|
-
*/
|
|
69
77
|
export declare function collectLift(skillDir: string): Lift[];
|
package/dist/lift.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { readResults, effectiveVerdicts } from "./results.js";
|
|
4
|
+
import { loadSpec } from "./spec.js";
|
|
4
5
|
/** A verdict that carries real evidence about the task, rather than about the harness or the judge. */
|
|
5
6
|
function conclusive(verdict, suspect) {
|
|
6
7
|
// ERROR is a harness failure (timeout, empty reply) — it says nothing about
|
|
@@ -30,9 +31,11 @@ function classify(red, green) {
|
|
|
30
31
|
* Both sides go through `effectiveVerdicts`, so an author override is what
|
|
31
32
|
* counts and an override resolves a misfire — the same rule scoring uses. Only
|
|
32
33
|
* the intersection of scenario ids is compared; a lift cannot speak to a
|
|
33
|
-
* scenario one side never ran
|
|
34
|
+
* scenario one side never ran, nor to one the harness ran identically in both
|
|
35
|
+
* modes (`opts.modeInsensitive`).
|
|
34
36
|
*/
|
|
35
|
-
export function computeLift(red, green) {
|
|
37
|
+
export function computeLift(red, green, opts = {}) {
|
|
38
|
+
const insensitive = new Set(opts.modeInsensitive ?? []);
|
|
36
39
|
const redV = new Map(effectiveVerdicts(red.scenarios).map((v) => [v.id, { verdict: v.verdict, suspect: v.suspect ?? false }]));
|
|
37
40
|
const greenV = new Map(effectiveVerdicts(green.scenarios).map((v) => [v.id, { verdict: v.verdict, suspect: v.suspect ?? false }]));
|
|
38
41
|
const cells = {};
|
|
@@ -41,10 +44,15 @@ export function computeLift(red, green) {
|
|
|
41
44
|
let greenPassed = 0;
|
|
42
45
|
// Green order drives display order (it is the run the author is looking at),
|
|
43
46
|
// restricted to ids the red baseline also covered.
|
|
47
|
+
const modeInsensitive = [];
|
|
44
48
|
for (const [id, g] of greenV) {
|
|
45
49
|
const r = redV.get(id);
|
|
46
50
|
if (!r)
|
|
47
51
|
continue;
|
|
52
|
+
if (insensitive.has(id)) {
|
|
53
|
+
modeInsensitive.push(id);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
48
56
|
const cls = classify(r, g);
|
|
49
57
|
cells[id] = { red: r.verdict, redSuspect: r.suspect, green: g.verdict, class: cls };
|
|
50
58
|
counts[cls]++;
|
|
@@ -71,14 +79,21 @@ export function computeLift(red, green) {
|
|
|
71
79
|
delta: greenPassed - redPassed,
|
|
72
80
|
greenOnly: [...greenV.keys()].filter((id) => !redV.has(id)),
|
|
73
81
|
redOnly: [...redV.keys()].filter((id) => !greenV.has(id)),
|
|
82
|
+
modeInsensitive,
|
|
74
83
|
partial: Boolean(red.partial || green.partial),
|
|
75
84
|
cells,
|
|
76
85
|
};
|
|
77
86
|
}
|
|
78
87
|
/** One line for a human: what the skill did, and what it cost. */
|
|
79
88
|
export function liftHeadline(lift) {
|
|
80
|
-
if (lift.compared === 0)
|
|
89
|
+
if (lift.compared === 0) {
|
|
90
|
+
// Excluded-but-shared is not the same as never-shared. Claiming the runs had
|
|
91
|
+
// no scenario in common would hide the reason the lift is empty.
|
|
92
|
+
if (lift.modeInsensitive.length > 0) {
|
|
93
|
+
return `nothing comparable (${lift.modeInsensitive.length} shared, all run identically in both modes)`;
|
|
94
|
+
}
|
|
81
95
|
return "no shared scenarios to compare";
|
|
96
|
+
}
|
|
82
97
|
// Everything inconclusive is NOT "no effect" — it is no measurement. Saying
|
|
83
98
|
// "no measured effect" here would be the same not-measured/measured-no-effect
|
|
84
99
|
// conflation this module refuses to make when a red baseline is missing.
|
|
@@ -98,6 +113,9 @@ export function liftHeadline(lift) {
|
|
|
98
113
|
}
|
|
99
114
|
if (lift.inconclusive > 0)
|
|
100
115
|
segments.push(`${lift.inconclusive} inconclusive`);
|
|
116
|
+
if (lift.modeInsensitive.length > 0) {
|
|
117
|
+
segments.push(`${lift.modeInsensitive.length} not comparable (same run in both modes)`);
|
|
118
|
+
}
|
|
101
119
|
if (lift.partial)
|
|
102
120
|
segments.push("partial run");
|
|
103
121
|
return segments.join(" · ");
|
|
@@ -124,10 +142,31 @@ function isDir(p) {
|
|
|
124
142
|
* A tag with no red baseline is omitted entirely rather than reported as a zero
|
|
125
143
|
* lift: "not measured" and "measured no effect" are different claims.
|
|
126
144
|
*/
|
|
145
|
+
/**
|
|
146
|
+
* Scenario ids the harness runs identically in red and green, read from the spec
|
|
147
|
+
* rather than from results.yaml: a lift is derived on read, so this has to work
|
|
148
|
+
* on runs recorded before the field existed — and `scenario:<id>` source hashes
|
|
149
|
+
* fold the value in without preserving it.
|
|
150
|
+
*
|
|
151
|
+
* Never throws: an unparseable spec must degrade to "nothing excluded" rather
|
|
152
|
+
* than take down a view that is otherwise readable from the results alone.
|
|
153
|
+
*/
|
|
154
|
+
function modeInsensitiveIds(skillDir) {
|
|
155
|
+
const specPath = join(skillDir, "tests", "specification.yaml");
|
|
156
|
+
if (!existsSync(specPath))
|
|
157
|
+
return [];
|
|
158
|
+
try {
|
|
159
|
+
return loadSpec(specPath).scenarios.filter((s) => s.systemPromptFile).map((s) => s.id);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
}
|
|
127
165
|
export function collectLift(skillDir) {
|
|
128
166
|
const resultsRoot = join(skillDir, "tests", "results");
|
|
129
167
|
if (!existsSync(resultsRoot))
|
|
130
168
|
return [];
|
|
169
|
+
const modeInsensitive = modeInsensitiveIds(skillDir);
|
|
131
170
|
const lifts = [];
|
|
132
171
|
for (const tag of readdirSync(resultsRoot).filter((n) => isDir(join(resultsRoot, n))).sort()) {
|
|
133
172
|
const tagDir = join(resultsRoot, tag);
|
|
@@ -156,7 +195,7 @@ export function collectLift(skillDir) {
|
|
|
156
195
|
}
|
|
157
196
|
if (!red || !green)
|
|
158
197
|
continue;
|
|
159
|
-
lifts.push({ ...computeLift(red, green), tag });
|
|
198
|
+
lifts.push({ ...computeLift(red, green, { modeInsensitive }), tag });
|
|
160
199
|
}
|
|
161
200
|
return lifts;
|
|
162
201
|
}
|
package/package.json
CHANGED
|
@@ -1,19 +1,45 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skill-harness/core",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "skill-harness engine — spec, discover, run, LLM-judge grade, score, results (internal API)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"main": "./dist/index.js",
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
9
|
-
"exports": {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"agent-skills",
|
|
14
|
+
"skill",
|
|
15
|
+
"llm",
|
|
16
|
+
"testing",
|
|
17
|
+
"eval",
|
|
18
|
+
"grading",
|
|
19
|
+
"pi",
|
|
20
|
+
"ci",
|
|
21
|
+
"harness"
|
|
22
|
+
],
|
|
23
|
+
"files": [
|
|
24
|
+
"dist/**/*.js",
|
|
25
|
+
"dist/**/*.d.ts",
|
|
26
|
+
"LICENSE",
|
|
27
|
+
"README.md"
|
|
28
|
+
],
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/mojomanyana/skill-harness.git"
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=20"
|
|
38
|
+
},
|
|
15
39
|
"scripts": {
|
|
16
40
|
"prepack": "cp ../../LICENSE ./LICENSE"
|
|
17
41
|
},
|
|
18
|
-
"dependencies": {
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"js-yaml": "^4.1.0"
|
|
44
|
+
}
|
|
19
45
|
}
|