skill-harness 0.13.0 → 0.14.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/README.md +4 -1
- package/assets/report.grade.js +177 -0
- package/assets/report.template.html +470 -0
- package/dist/index.js +17311 -0
- package/dist/prompt-capture-extension.js +118 -0
- package/package.json +10 -2
package/README.md
CHANGED
|
@@ -8,9 +8,12 @@ bar, and open an interactive review UI so you can measure a `SKILL.md` edit.
|
|
|
8
8
|
## Install
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
|
-
npm i -g skill-harness
|
|
11
|
+
npm i -g skill-harness # CLI
|
|
12
|
+
pi install npm:skill-harness # CLI plus /skill-harness extension in Pi
|
|
12
13
|
```
|
|
13
14
|
|
|
15
|
+
The Pi package declares its bundled extension directly. After install/update, `/reload` makes it available in an existing supported Pi session; verify the loaded resource rather than treating the package version as proof.
|
|
16
|
+
|
|
14
17
|
## The loop
|
|
15
18
|
|
|
16
19
|
1. **list** — discover which skills have a spec.
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// Client-side scorer for the review UI (assets/report.template.html).
|
|
2
|
+
//
|
|
3
|
+
// This is the SINGLE source of truth for "how does a column get graded in the
|
|
4
|
+
// browser" — it must implement exactly the same rules as
|
|
5
|
+
// packages/core/src/score.ts's `score()`. It is:
|
|
6
|
+
// (a) imported directly, as plain ESM, by
|
|
7
|
+
// packages/core/test/grade-column-parity.test.ts, which asserts parity
|
|
8
|
+
// against score.ts for a set of fixtures (PASS/FAIL/critical/B-series/
|
|
9
|
+
// suspect/override-resolved-suspect). If you change score.ts's rules
|
|
10
|
+
// and forget to mirror them here, that test fails.
|
|
11
|
+
// (b) injected verbatim into report.template.html's inline <script>, at the
|
|
12
|
+
// GRADE placeholder comment near its top (see renderReport in
|
|
13
|
+
// packages/core/src/report.ts).
|
|
14
|
+
//
|
|
15
|
+
// INJECTION NOTE: a bare inline <script> (no type="module") cannot contain an
|
|
16
|
+
// `export` statement. renderReport() strips the leading `export ` keyword off
|
|
17
|
+
// each exported declaration textually before splicing this file's contents
|
|
18
|
+
// into the template. Nothing here relies on import/export semantics at
|
|
19
|
+
// runtime (no imports, no re-exports), so stripping `export ` and leaving
|
|
20
|
+
// plain function declarations behind is safe in both the browser (global
|
|
21
|
+
// script scope) and Node (this file imported as an ES module).
|
|
22
|
+
//
|
|
23
|
+
// No DOM access, no imports — must load in Node (for the parity test) and in
|
|
24
|
+
// a plain <script> in the browser (for the review UI).
|
|
25
|
+
|
|
26
|
+
export function effective(cell) {
|
|
27
|
+
if (cell.override) return cell.override;
|
|
28
|
+
// Mechanical evidence outranks a prose judge. Objective PASS deliberately
|
|
29
|
+
// forces nothing — the checklist judge still decides the behavioral rubric.
|
|
30
|
+
if (cell.objective && cell.objective.status === "ERROR") return "ERROR";
|
|
31
|
+
if (cell.objective && cell.objective.status === "NOT-MEASURED") return "NOT-MEASURED";
|
|
32
|
+
if (cell.objective && cell.objective.status === "FAIL") return "FAIL";
|
|
33
|
+
return cell.judge_verdict;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function letterFor(pct) {
|
|
37
|
+
if (pct >= 90) return "A";
|
|
38
|
+
if (pct >= 80) return "B";
|
|
39
|
+
if (pct >= 70) return "C";
|
|
40
|
+
if (pct >= 60) return "D";
|
|
41
|
+
return "F";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Score one report column against the ship bar — mirrors score.ts's `score()`
|
|
46
|
+
* exactly, over `col.cells` (a scenario-id -> cell map) instead of a flat
|
|
47
|
+
* verdict list. A `suspect` cell without an override is excluded from both
|
|
48
|
+
* `passed` and `total` (untrustworthy: neither pass nor fail) and blocks ship.
|
|
49
|
+
*/
|
|
50
|
+
export function gradeColumn(col, shipBar, critical) {
|
|
51
|
+
let passed = 0;
|
|
52
|
+
let total = 0;
|
|
53
|
+
let criticalFails = 0;
|
|
54
|
+
let bFails = 0;
|
|
55
|
+
let suspect = 0;
|
|
56
|
+
let errors = 0;
|
|
57
|
+
let notMeasured = 0;
|
|
58
|
+
|
|
59
|
+
for (const id of Object.keys(col.cells)) {
|
|
60
|
+
const cell = col.cells[id];
|
|
61
|
+
if (!cell) continue;
|
|
62
|
+
if (cell.suspect && !cell.override) {
|
|
63
|
+
suspect++;
|
|
64
|
+
continue; // excluded, blocks ship
|
|
65
|
+
}
|
|
66
|
+
const verdict = effective(cell);
|
|
67
|
+
if (verdict === "NOT-MEASURED") {
|
|
68
|
+
notMeasured++;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (verdict === "ERROR" || verdict === "JUDGE-AMBIGUOUS") {
|
|
72
|
+
errors++;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
total++;
|
|
76
|
+
if (verdict === "PASS") {
|
|
77
|
+
passed++;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (critical.includes(id)) criticalFails++;
|
|
81
|
+
if (/^B/i.test(id)) bFails++;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (col.partial === true) {
|
|
85
|
+
return { passed: 0, total: 0, pct: 0, letter: "-", ship: false, criticalFails, bFails, suspect, errors, notMeasured };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const pct = total > 0 ? Math.round((passed * 100) / total) : 0;
|
|
89
|
+
const letter = letterFor(pct);
|
|
90
|
+
const validBar = Number.isInteger(shipBar.total) && shipBar.total >= 1 && Number.isInteger(shipBar.min_pass) && shipBar.min_pass >= 1 && shipBar.min_pass <= shipBar.total;
|
|
91
|
+
const ship =
|
|
92
|
+
validBar &&
|
|
93
|
+
total >= shipBar.total &&
|
|
94
|
+
passed >= shipBar.min_pass &&
|
|
95
|
+
(!shipBar.no_critical_fail || criticalFails === 0) &&
|
|
96
|
+
bFails === 0 &&
|
|
97
|
+
suspect === 0 &&
|
|
98
|
+
errors === 0 &&
|
|
99
|
+
notMeasured === 0;
|
|
100
|
+
|
|
101
|
+
return { passed, total, pct, letter, ship, criticalFails, bFails, suspect, errors, notMeasured };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Red-vs-green class for ONE scenario — mirrors `classify` in
|
|
106
|
+
* packages/core/src/lift.ts exactly. Kept in the client because the author
|
|
107
|
+
* flips green verdicts live in this UI: a server-computed class would freeze at
|
|
108
|
+
* page load and could show "gained" beside a cell the author just marked FAIL.
|
|
109
|
+
*
|
|
110
|
+
* `liftCell` supplies the red side (verdict + redSuspect, from the baseline run,
|
|
111
|
+
* which this view never edits); `greenCell` is the live report cell, so its
|
|
112
|
+
* override and suspect state are read through the same effective()/suspect rule
|
|
113
|
+
* the grader uses.
|
|
114
|
+
*/
|
|
115
|
+
export function liftClass(liftCell, greenCell) {
|
|
116
|
+
const conclusive = (verdict, suspect) =>
|
|
117
|
+
!suspect && verdict !== "ERROR" && verdict !== "NOT-MEASURED" && verdict !== "JUDGE-AMBIGUOUS";
|
|
118
|
+
|
|
119
|
+
const redOk = conclusive(liftCell.red, liftCell.redSuspect);
|
|
120
|
+
const greenOk = conclusive(effective(greenCell), !!greenCell.suspect && !greenCell.override);
|
|
121
|
+
if (!redOk || !greenOk) return "inconclusive";
|
|
122
|
+
|
|
123
|
+
const redPass = liftCell.red === "PASS";
|
|
124
|
+
const greenPass = effective(greenCell) === "PASS";
|
|
125
|
+
if (redPass && greenPass) return "kept";
|
|
126
|
+
if (!redPass && greenPass) return "gained";
|
|
127
|
+
if (redPass && !greenPass) return "regressed";
|
|
128
|
+
return "both-fail";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Aggregate a column's lift over the live cells — mirrors computeLift's counters
|
|
133
|
+
* in packages/core/src/lift.ts. Only scenarios the baseline also covered are
|
|
134
|
+
* counted, matching the server's intersection rule.
|
|
135
|
+
*/
|
|
136
|
+
export function liftSummary(col) {
|
|
137
|
+
const out = { gained: 0, regressed: 0, kept: 0, bothFail: 0, inconclusive: 0, compared: 0, redPassed: 0, greenPassed: 0, delta: 0 };
|
|
138
|
+
if (!col.lift) return null;
|
|
139
|
+
for (const id of Object.keys(col.lift.cells)) {
|
|
140
|
+
const greenCell = col.cells[id];
|
|
141
|
+
if (!greenCell) continue;
|
|
142
|
+
const liftCell = col.lift.cells[id];
|
|
143
|
+
const cls = liftClass(liftCell, greenCell);
|
|
144
|
+
out.compared++;
|
|
145
|
+
if (cls === "both-fail") out.bothFail++;
|
|
146
|
+
else out[cls]++;
|
|
147
|
+
if (cls !== "inconclusive") {
|
|
148
|
+
if (liftCell.red === "PASS") out.redPassed++;
|
|
149
|
+
if (effective(greenCell) === "PASS") out.greenPassed++;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
out.delta = out.greenPassed - out.redPassed;
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The lift badge for a column with nothing to show in the number: `{text, title}`,
|
|
158
|
+
* or null when the column should carry no badge at all.
|
|
159
|
+
*
|
|
160
|
+
* A *missing* baseline and an *unusable* one are different facts. Nothing is
|
|
161
|
+
* comparable when every shared scenario ran identically in both modes, or when the
|
|
162
|
+
* two sides aggregated differently (red at 1 rep vs green at 3) — a red baseline
|
|
163
|
+
* exists, and "no red baseline" would send the author off to re-run the one thing
|
|
164
|
+
* they already have. The server's headline already states which it is and what to
|
|
165
|
+
* re-run, so it becomes the tooltip verbatim.
|
|
166
|
+
*/
|
|
167
|
+
export function liftNoneBadge(col) {
|
|
168
|
+
if (col.lift) {
|
|
169
|
+
return { text: "lift not comparable", title: col.liftHeadline || "nothing in the red baseline could be compared" };
|
|
170
|
+
}
|
|
171
|
+
// Green and force are both skill-delivered, so both can be missing a baseline.
|
|
172
|
+
// A red column is the baseline and gets no badge at all.
|
|
173
|
+
if (col.mode === "green" || col.mode === "force") {
|
|
174
|
+
return { text: "no red baseline", title: "run the same scenarios with --mode red to get a baseline" };
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|