@skill-harness/core 0.4.0 → 0.6.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/dist/adapters/types.d.ts +10 -0
- package/dist/canary.d.ts +44 -0
- package/dist/canary.js +123 -0
- package/dist/discover.d.ts +7 -0
- package/dist/discover.js +13 -5
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/journal.d.ts +14 -0
- package/dist/lift.d.ts +13 -0
- package/dist/lift.js +13 -8
- package/dist/lint.d.ts +16 -1
- package/dist/lint.js +39 -3
- package/dist/regate.js +15 -11
- package/dist/regrade.d.ts +21 -11
- package/dist/regrade.js +26 -19
- package/dist/report.d.ts +30 -5
- package/dist/report.js +20 -5
- package/dist/rescore.d.ts +6 -0
- package/dist/rescore.js +12 -4
- package/dist/results.d.ts +78 -0
- package/dist/results.js +51 -0
- package/dist/run.d.ts +16 -1
- package/dist/run.js +75 -8
- package/dist/sources.d.ts +26 -0
- package/dist/sources.js +42 -0
- package/dist/stability.d.ts +144 -0
- package/dist/stability.js +232 -0
- package/dist/trends.d.ts +51 -9
- package/dist/trends.js +89 -65
- package/package.json +1 -1
package/dist/trends.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { loadSpec } from "./spec.js";
|
|
4
|
-
import { readResults, effectiveVerdicts } from "./results.js";
|
|
4
|
+
import { readResults, effectiveVerdicts, isScoredMode } from "./results.js";
|
|
5
5
|
/** A directory that exists right now; false (never throws) if it vanished concurrently (e.g. ENOENT). */
|
|
6
6
|
function isDir(p) {
|
|
7
7
|
try {
|
|
@@ -11,6 +11,67 @@ function isDir(p) {
|
|
|
11
11
|
return false;
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Walk `<skillDir>/tests/results/` and group every SCORED run by model tag × delivery
|
|
16
|
+
* mode, chronologically (timestamp-slug dir names sort correctly).
|
|
17
|
+
*
|
|
18
|
+
* The single history reader: `collectTrends` renders it, `collectStability` derives
|
|
19
|
+
* run-over-run flips from it. Two walkers over the same tree is how "which runs count"
|
|
20
|
+
* drifts — the mistake that had force runs excluded from scoring in seven places at
|
|
21
|
+
* once (see SCORED_MODES).
|
|
22
|
+
*
|
|
23
|
+
* Red runs are excluded: a baseline has no grade, and pairing it with anything would
|
|
24
|
+
* compare a skill-off run to a skill-on one. Green and force are never pooled into one
|
|
25
|
+
* group — placement moves verdicts, so a green run and a force run of the same scenario
|
|
26
|
+
* are two measurements, not two samples.
|
|
27
|
+
*
|
|
28
|
+
* A run whose `results.yaml` fails to parse (e.g. an interrupted non-atomic write) is
|
|
29
|
+
* logged via `console.warn`, skipped, and counted in `skipped` — never thrown, because
|
|
30
|
+
* one torn file must not take down a whole read-only view.
|
|
31
|
+
*/
|
|
32
|
+
export function collectScoredRuns(skillDir) {
|
|
33
|
+
const resultsRoot = join(skillDir, "tests", "results");
|
|
34
|
+
if (!existsSync(resultsRoot))
|
|
35
|
+
return [];
|
|
36
|
+
const groups = [];
|
|
37
|
+
const tags = readdirSync(resultsRoot)
|
|
38
|
+
.filter((n) => isDir(join(resultsRoot, n)))
|
|
39
|
+
.sort();
|
|
40
|
+
for (const tag of tags) {
|
|
41
|
+
const tagDir = join(resultsRoot, tag);
|
|
42
|
+
const runDirs = readdirSync(tagDir)
|
|
43
|
+
.map((n) => join(tagDir, n))
|
|
44
|
+
.filter((p) => isDir(p) && existsSync(join(p, "results.yaml")))
|
|
45
|
+
.sort(); // timestamp-slug dir names ⇒ chronological ascending
|
|
46
|
+
if (runDirs.length === 0)
|
|
47
|
+
continue;
|
|
48
|
+
// Every candidate run is read: a run's mode is not knowable from its dir name, so
|
|
49
|
+
// filtering has to happen after the read. Bucketed by mode in first-seen order.
|
|
50
|
+
const byMode = new Map();
|
|
51
|
+
let skipped = 0;
|
|
52
|
+
for (const rd of runDirs) {
|
|
53
|
+
let r;
|
|
54
|
+
try {
|
|
55
|
+
r = readResults(rd);
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
console.warn(`skill-harness: skipping unreadable run ${rd}: ${e instanceof Error ? e.message : e}`);
|
|
59
|
+
skipped++;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (!isScoredMode(r.mode))
|
|
63
|
+
continue; // baseline — deliberate exclusion, not a skip
|
|
64
|
+
(byMode.get(r.mode) ?? byMode.set(r.mode, []).get(r.mode)).push(r);
|
|
65
|
+
}
|
|
66
|
+
for (const [mode, runs] of byMode) {
|
|
67
|
+
// `skipped` is per tag (an unreadable run has no knowable mode), so a tag with
|
|
68
|
+
// two series reports the same count on both — the alternative is attributing a
|
|
69
|
+
// parse failure to a mode nobody could read.
|
|
70
|
+
groups.push({ tag, mode, model: runs[runs.length - 1].model, runs, skipped });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return groups;
|
|
74
|
+
}
|
|
14
75
|
/**
|
|
15
76
|
* Per model-tag, read the full run history (not just the latest) from
|
|
16
77
|
* <skillDir>/tests/results/, chronologically (timestamp-slug dir names sort
|
|
@@ -19,15 +80,18 @@ function isDir(p) {
|
|
|
19
80
|
* rule: an override resolves a misfire) + reps flakiness. Read-only; no
|
|
20
81
|
* absolute paths in the result.
|
|
21
82
|
*
|
|
22
|
-
* Only scored
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
83
|
+
* Only scored runs are included in the history — a red baseline has no real grade
|
|
84
|
+
* (`effective_grade` is a "not scored" placeholder; see run.ts) and would otherwise
|
|
85
|
+
* plot as a misleading 0% dip in the sparkline/grid. Red runs are deliberately
|
|
86
|
+
* excluded, which is distinct from `skipped`: a run's mode can only be known after
|
|
87
|
+
* reading its results.yaml, so every candidate run-dir in the tag is read (not just
|
|
88
|
+
* the most recent `limit`) before filtering and applying the `limit` window —
|
|
89
|
+
* trends is a bounded, on-demand, local view, so this extra read cost is
|
|
90
|
+
* acceptable.
|
|
91
|
+
*
|
|
92
|
+
* Green and force runs both count, but never in the same series: a tag with both
|
|
93
|
+
* yields one TrendModel per mode (see `TrendModel.mode`), each with its own
|
|
94
|
+
* `limit` window. A tag with no scored run at all is omitted entirely.
|
|
31
95
|
*
|
|
32
96
|
* A run whose `results.yaml` fails to parse (e.g. an interrupted non-atomic
|
|
33
97
|
* write) is logged via `console.warn` and skipped — never surfaced or thrown —
|
|
@@ -39,63 +103,23 @@ export function collectTrends(skillDir, limit = 20) {
|
|
|
39
103
|
const specPath = join(skillDir, "tests", "specification.yaml");
|
|
40
104
|
const spec = loadSpec(specPath);
|
|
41
105
|
const scenarios = spec.scenarios.map((s) => ({ id: s.id, title: s.title, critical: s.critical }));
|
|
42
|
-
const resultsRoot = join(skillDir, "tests", "results");
|
|
43
106
|
const models = [];
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
for (const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
// filtering after the slice would let red/force runs consume window
|
|
59
|
-
// slots, undercounting the green history even when more exists.
|
|
60
|
-
const greenRuns = [];
|
|
61
|
-
let skipped = 0;
|
|
62
|
-
for (const rd of runDirs) {
|
|
63
|
-
let r;
|
|
64
|
-
try {
|
|
65
|
-
r = readResults(rd);
|
|
66
|
-
}
|
|
67
|
-
catch (e) {
|
|
68
|
-
// A corrupt/truncated results.yaml must not take down the whole
|
|
69
|
-
// trends view — skip that run, but surface the failure.
|
|
70
|
-
console.warn(`skill-harness trends: skipping unreadable run ${rd}: ${e instanceof Error ? e.message : e}`);
|
|
71
|
-
skipped++;
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
if (r.mode !== "green")
|
|
75
|
-
continue; // not scored — deliberate exclusion, not a skip
|
|
76
|
-
greenRuns.push(r);
|
|
77
|
-
}
|
|
78
|
-
if (greenRuns.length === 0)
|
|
79
|
-
continue;
|
|
80
|
-
const truncated = greenRuns.length > limit;
|
|
81
|
-
const kept = greenRuns.slice(-limit); // most recent `limit`, newest last
|
|
82
|
-
const runs = [];
|
|
83
|
-
let model = "";
|
|
84
|
-
for (const r of kept) {
|
|
85
|
-
// effectiveVerdicts is the single source of truth for the
|
|
86
|
-
// override-aware verdict/suspect rule (suspect = s.suspect &&
|
|
87
|
-
// s.override == null — an override resolves the misfire); zip in
|
|
88
|
-
// flakiness from the matching ScenarioResult.
|
|
89
|
-
const verdicts = effectiveVerdicts(r.scenarios);
|
|
90
|
-
const cells = {};
|
|
91
|
-
r.scenarios.forEach((s, i) => {
|
|
92
|
-
cells[s.id] = { verdict: verdicts[i].verdict, suspect: verdicts[i].suspect ?? false, flakiness: s.flakiness };
|
|
93
|
-
});
|
|
94
|
-
runs.push({ timestamp: r.timestamp, label: r.label, grade: r.effective_grade, cells });
|
|
95
|
-
model = r.model; // last successfully-read run (kept is ascending) wins
|
|
96
|
-
}
|
|
97
|
-
models.push({ model, tag, runs, truncated, skipped });
|
|
107
|
+
for (const group of collectScoredRuns(skillDir)) {
|
|
108
|
+
const truncated = group.runs.length > limit;
|
|
109
|
+
const kept = group.runs.slice(-limit); // most recent `limit`, newest last
|
|
110
|
+
const runs = [];
|
|
111
|
+
for (const r of kept) {
|
|
112
|
+
// effectiveVerdicts is the single source of truth for the override-aware
|
|
113
|
+
// verdict/suspect rule (suspect = s.suspect && s.override == null — an override
|
|
114
|
+
// resolves the misfire); zip in flakiness from the matching ScenarioResult.
|
|
115
|
+
const verdicts = effectiveVerdicts(r.scenarios);
|
|
116
|
+
const cells = {};
|
|
117
|
+
r.scenarios.forEach((s, i) => {
|
|
118
|
+
cells[s.id] = { verdict: verdicts[i].verdict, suspect: verdicts[i].suspect ?? false, flakiness: s.flakiness };
|
|
119
|
+
});
|
|
120
|
+
runs.push({ timestamp: r.timestamp, label: r.label, grade: r.effective_grade, cells });
|
|
98
121
|
}
|
|
122
|
+
models.push({ model: group.model, tag: group.tag, mode: group.mode, runs, truncated, skipped: group.skipped });
|
|
99
123
|
}
|
|
100
124
|
return { skill: spec.skill, scenarios, models };
|
|
101
125
|
}
|