skill-probe 0.8.0 → 0.9.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 +60 -8
- package/dist/baseline.d.ts +56 -0
- package/dist/baseline.js +0 -0
- package/dist/cli.js +23 -4
- package/dist/context.d.ts +3 -0
- package/dist/context.js +2 -0
- package/dist/diagnose.d.ts +3 -0
- package/dist/diagnose.js +2 -0
- package/dist/eval.d.ts +3 -0
- package/dist/eval.js +5 -3
- package/dist/manifest.d.ts +21 -0
- package/dist/manifest.js +48 -0
- package/dist/report.d.ts +3 -0
- package/dist/report.js +33 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,7 +27,8 @@ doctor → gen → audit → context → diagnose → fix
|
|
|
27
27
|
|
|
28
28
|
- `doctor`: verify setup, auth, runtime, skills, and config before spending probes.
|
|
29
29
|
- `gen`: draft a probe config from your existing skills.
|
|
30
|
-
- `audit`: measure which skill actually fires when the whole library is co-loaded
|
|
30
|
+
- `audit`: measure which skill actually fires when the whole library is co-loaded — with
|
|
31
|
+
`--baseline`, gate CI on statistically significant regressions only.
|
|
31
32
|
- `context`: compare isolation vs co-loaded activation to catch library interference —
|
|
32
33
|
add `--ablate` to name **which** sibling is stealing the trigger.
|
|
33
34
|
- `diagnose`: explain whether a failure is a routing miss or a description problem.
|
|
@@ -157,16 +158,64 @@ Claude Code vs OpenCode comparison is fair — otherwise you're comparing two co
|
|
|
157
158
|
|
|
158
159
|
## CI usage
|
|
159
160
|
|
|
160
|
-
skill-probe exits non-zero on a real problem, so it drops straight into a pipeline:
|
|
161
|
+
skill-probe exits non-zero on a real problem, so it drops straight into a pipeline. Exit codes:
|
|
162
|
+
**`0`** all pass · **`1`** a behavioral fail / trigger-theft / interference / regression · **`2`**
|
|
163
|
+
inconclusive or an infrastructure error — so a runtime outage *fails* the build instead of silently
|
|
164
|
+
passing. Add `--json` to archive the full result (it includes a **run manifest**: tool version,
|
|
165
|
+
model, date, config hash — so two runs are comparable and a report is citable).
|
|
166
|
+
|
|
167
|
+
### Regression gating (`--baseline`)
|
|
168
|
+
|
|
169
|
+
The CI question isn't "are my skills perfect?" — it's **"did this PR make any skill worse?"**
|
|
170
|
+
Activation is stochastic, so comparing raw rates makes CI flaky. The baseline gate compares each
|
|
171
|
+
case against a saved baseline with **Fisher's exact test, Benjamini-Hochberg corrected** — noise
|
|
172
|
+
passes, real drops fail:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
# once, on a good main build (commit the file):
|
|
176
|
+
skill-probe --config probe.config.json --save-baseline baselines/main.json
|
|
177
|
+
|
|
178
|
+
# on every PR:
|
|
179
|
+
skill-probe --config probe.config.json --baseline baselines/main.json
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
Baseline gate — vs baseline saved 2026-07-02T09:12:03Z (skill-probe 0.9.0)
|
|
184
|
+
[▼ REGRESSED] greeter | write a birthday greeting
|
|
185
|
+
90% (9/10) → 20% (2/10) Δ-70% p=0.005 · adj 0.005
|
|
186
|
+
Gate: ▼ FAIL — 1 significant regression(s)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
The gate warns (but still runs, matching cases by prompt) if the config, runtime, or model changed
|
|
190
|
+
since the baseline — apples-to-oranges comparisons are flagged, never silent. Significant
|
|
191
|
+
*improvements* are reported too, as a nudge to re-save the baseline.
|
|
192
|
+
|
|
193
|
+
### GitHub Action
|
|
161
194
|
|
|
162
195
|
```yaml
|
|
163
|
-
# .github/workflows/skills.yml
|
|
164
|
-
|
|
196
|
+
# .github/workflows/skills.yml
|
|
197
|
+
name: skills
|
|
198
|
+
on: pull_request
|
|
199
|
+
jobs:
|
|
200
|
+
probe:
|
|
201
|
+
runs-on: ubuntu-latest
|
|
202
|
+
steps:
|
|
203
|
+
- uses: actions/checkout@v4
|
|
204
|
+
- uses: actions/setup-node@v4
|
|
205
|
+
with: { node-version: "22" }
|
|
206
|
+
- uses: HystonKayange/skill-probe@main
|
|
207
|
+
with:
|
|
208
|
+
config: probe.config.json
|
|
209
|
+
args: --baseline baselines/main.json
|
|
210
|
+
env:
|
|
211
|
+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
165
212
|
```
|
|
166
213
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
214
|
+
The action installs skill-probe plus the runtime CLI (Claude Code by default; set
|
|
215
|
+
`runtime-package: ""` if your runner already has one) and runs the command. `command:` selects
|
|
216
|
+
`audit` (default), `context`, `diagnose`, or `doctor`; `args:` passes extra flags like `--ablate`.
|
|
217
|
+
The runtime authenticates via `ANTHROPIC_API_KEY` — headless probes each cost real tokens, so
|
|
218
|
+
smoke settings (`k: 10`, few cases) are the sweet spot for per-PR gating.
|
|
170
219
|
|
|
171
220
|
## Activation rate by context (`skill-probe context`)
|
|
172
221
|
|
|
@@ -320,7 +369,10 @@ should-fire prompts, near-misses, and decoys.
|
|
|
320
369
|
(pass / fail / inconclusive / **error**), across two runtimes (Claude Code, OpenCode).
|
|
321
370
|
Infrastructure failures (timeout / auth / crash / empty output / a zero-cost response where the
|
|
322
371
|
model never actually ran, e.g. a usage-limit window) are reported as `error`, never as a
|
|
323
|
-
behavioral pass/fail — a decoy can't falsely pass because the runtime was down.
|
|
372
|
+
behavioral pass/fail — a decoy can't falsely pass because the runtime was down. Every run carries
|
|
373
|
+
a **manifest** (version, model, date, config hash); `--save-baseline` / `--baseline` turn audits
|
|
374
|
+
into a CI regression gate (Fisher + BH — noise passes, real drops fail), and a **GitHub Action**
|
|
375
|
+
(`HystonKayange/skill-probe@main`) wraps the whole thing for per-PR gating.
|
|
324
376
|
|
|
325
377
|
**Context (`skill-probe context`):** isolation-vs-co-loaded activation rates, with **Fisher's exact
|
|
326
378
|
test** on the drop and **Benjamini-Hochberg correction** across cases — catches skills that fire
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { AuditResult } from "./eval.ts";
|
|
2
|
+
import type { RunManifest } from "./manifest.ts";
|
|
3
|
+
export interface BaselineCase {
|
|
4
|
+
prompt: string;
|
|
5
|
+
expected: string | null;
|
|
6
|
+
hits: number;
|
|
7
|
+
n: number;
|
|
8
|
+
}
|
|
9
|
+
export interface BaselineFile {
|
|
10
|
+
manifest: RunManifest;
|
|
11
|
+
cases: BaselineCase[];
|
|
12
|
+
}
|
|
13
|
+
export declare function toBaseline(result: AuditResult): BaselineFile;
|
|
14
|
+
export declare function saveBaseline(result: AuditResult, path: string): void;
|
|
15
|
+
export declare function loadBaseline(path: string): BaselineFile;
|
|
16
|
+
export interface RegressionCase {
|
|
17
|
+
prompt: string;
|
|
18
|
+
expected: string | null;
|
|
19
|
+
before: {
|
|
20
|
+
hits: number;
|
|
21
|
+
n: number;
|
|
22
|
+
pHat: number;
|
|
23
|
+
};
|
|
24
|
+
after: {
|
|
25
|
+
hits: number;
|
|
26
|
+
n: number;
|
|
27
|
+
pHat: number;
|
|
28
|
+
};
|
|
29
|
+
/** after.pHat - before.pHat (negative = dropped since baseline) */
|
|
30
|
+
delta: number;
|
|
31
|
+
fisherP: number;
|
|
32
|
+
/** BH-adjusted across all compared cases — regression is decided on THIS */
|
|
33
|
+
fisherPAdj: number;
|
|
34
|
+
/** one side has no valid probes — not comparable (never reads as "no regression") */
|
|
35
|
+
untrustworthy: boolean;
|
|
36
|
+
/** significant drop since baseline (BH-corrected) */
|
|
37
|
+
regressed: boolean;
|
|
38
|
+
/** significant rise since baseline (informational — consider re-saving the baseline) */
|
|
39
|
+
improved: boolean;
|
|
40
|
+
}
|
|
41
|
+
export interface BaselineComparison {
|
|
42
|
+
baselineDate: string;
|
|
43
|
+
baselineVersion: string;
|
|
44
|
+
/** apples-to-oranges warnings: config hash / runtime / model drift between runs */
|
|
45
|
+
mismatches: string[];
|
|
46
|
+
cases: RegressionCase[];
|
|
47
|
+
/** prompts in the current run with no baseline entry (measured, but not gated) */
|
|
48
|
+
newCases: string[];
|
|
49
|
+
/** prompts in the baseline that the current run no longer has */
|
|
50
|
+
missingCases: string[];
|
|
51
|
+
regressions: number;
|
|
52
|
+
improvements: number;
|
|
53
|
+
/** 1 = at least one significant regression (CI should fail) */
|
|
54
|
+
exitCode: 0 | 1;
|
|
55
|
+
}
|
|
56
|
+
export declare function compareToBaseline(result: AuditResult, base: BaselineFile): BaselineComparison;
|
package/dist/baseline.js
ADDED
|
Binary file
|
package/dist/cli.js
CHANGED
|
@@ -12,7 +12,8 @@ import { runDiagnose } from "./diagnose.js";
|
|
|
12
12
|
import { runDoctor } from "./doctor.js";
|
|
13
13
|
import { listSkills, generateCases, genConfig } from "./gen.js";
|
|
14
14
|
import { loadConfig, parseProbability, parseIntFlag, ConfigError } from "./config.js";
|
|
15
|
-
import {
|
|
15
|
+
import { loadBaseline, saveBaseline, compareToBaseline } from "./baseline.js";
|
|
16
|
+
import { renderTable, renderMarkdown, renderJson, renderFix, renderContext, renderDoctor, renderDiagnose, renderBaseline } from "./report.js";
|
|
16
17
|
const HELP = `skill-probe — audit a co-loaded agent skill library by real activation behavior.
|
|
17
18
|
|
|
18
19
|
USAGE
|
|
@@ -36,6 +37,11 @@ OPTIONS
|
|
|
36
37
|
--markdown emit a Markdown table (paste into a PR/README)
|
|
37
38
|
--no-cost hide the cost line (e.g. when on a Claude subscription)
|
|
38
39
|
--quiet suppress the live progress line on stderr
|
|
40
|
+
audit only (CI gating):
|
|
41
|
+
--save-baseline <f> after the run, save per-case counts + a run manifest to <f>
|
|
42
|
+
--baseline <f> compare this run against a saved baseline: per-case Fisher's exact,
|
|
43
|
+
Benjamini-Hochberg corrected — exit 1 ONLY on a significant regression
|
|
44
|
+
(noise passes; real drops fail)
|
|
39
45
|
context only:
|
|
40
46
|
--ablate leave-one-out: for each interference case, re-run with each suspect
|
|
41
47
|
sibling removed and NAME the thief when activation recovers (Fisher +
|
|
@@ -106,10 +112,21 @@ async function audit(values) {
|
|
|
106
112
|
const result = await runAudit(cfg, makeProgress(values));
|
|
107
113
|
clearProgress();
|
|
108
114
|
const showCost = !values["no-cost"];
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
115
|
+
let cmp;
|
|
116
|
+
if (values["baseline"])
|
|
117
|
+
cmp = compareToBaseline(result, loadBaseline(values["baseline"]));
|
|
118
|
+
if (values["save-baseline"]) {
|
|
119
|
+
saveBaseline(result, values["save-baseline"]);
|
|
120
|
+
console.error(`baseline saved to ${values["save-baseline"]}`);
|
|
121
|
+
}
|
|
122
|
+
const text = values["json"] ? renderJson(cmp ? { ...result, baseline: cmp } : result)
|
|
123
|
+
: values["markdown"] ? renderMarkdown(result, { showCost }) + (cmp ? "\n\n" + renderBaseline(cmp) : "")
|
|
124
|
+
: renderTable(result, { showCost }) + (cmp ? "\n\n" + renderBaseline(cmp) : "");
|
|
112
125
|
console.log(text);
|
|
126
|
+
// precedence mirrors the rest of the tool: a behavioral signal (fail/regression, 1) outranks
|
|
127
|
+
// inconclusive/infra (2), which outranks clean (0).
|
|
128
|
+
if (result.exitCode === 1 || cmp?.exitCode === 1)
|
|
129
|
+
return 1;
|
|
113
130
|
return result.exitCode;
|
|
114
131
|
}
|
|
115
132
|
async function diagnose(values) {
|
|
@@ -204,6 +221,8 @@ async function main() {
|
|
|
204
221
|
decoys: { type: "string" },
|
|
205
222
|
ablate: { type: "boolean", default: false },
|
|
206
223
|
suspects: { type: "string" },
|
|
224
|
+
baseline: { type: "string" },
|
|
225
|
+
"save-baseline": { type: "string" },
|
|
207
226
|
"skip-probe": { type: "boolean", default: false },
|
|
208
227
|
json: { type: "boolean", default: false },
|
|
209
228
|
markdown: { type: "boolean", default: false },
|
package/dist/context.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Config } from "./types.ts";
|
|
2
2
|
import { reliability } from "./stats.ts";
|
|
3
3
|
import { type RunStats } from "./orchestrator.ts";
|
|
4
|
+
import { type RunManifest } from "./manifest.ts";
|
|
4
5
|
/** Build a throwaway project dir whose .claude/skills/ holds ONLY the named skills (copied from
|
|
5
6
|
* srcCwd). Returns its path plus a cleanup(). The probe runs against this dir to load a subset. */
|
|
6
7
|
export declare function buildContextDir(srcCwd: string, skillNames: string[]): {
|
|
@@ -56,6 +57,8 @@ export interface ContextAuditResult {
|
|
|
56
57
|
totalCost: number;
|
|
57
58
|
/** 0 = no interference; 1 = a skill is suppressed under load; 2 = an untrustworthy (infra) case */
|
|
58
59
|
exitCode: 0 | 1 | 2;
|
|
60
|
+
/** who/what/when of this run — makes reports citable and comparable */
|
|
61
|
+
manifest: RunManifest;
|
|
59
62
|
}
|
|
60
63
|
export interface ContextProgress {
|
|
61
64
|
caseIndex: number;
|
package/dist/context.js
CHANGED
|
@@ -11,6 +11,7 @@ import { measure, infraUntrustworthy } from "./orchestrator.js";
|
|
|
11
11
|
import { getAdapter } from "./adapters/index.js";
|
|
12
12
|
import { listSkills } from "./gen.js";
|
|
13
13
|
import { ConfigError } from "./config.js";
|
|
14
|
+
import { buildManifest } from "./manifest.js";
|
|
14
15
|
/** Build a throwaway project dir whose .claude/skills/ holds ONLY the named skills (copied from
|
|
15
16
|
* srcCwd). Returns its path plus a cleanup(). The probe runs against this dir to load a subset. */
|
|
16
17
|
export function buildContextDir(srcCwd, skillNames) {
|
|
@@ -167,5 +168,6 @@ export async function runContextAudit(cfg, optsOrProgress = {}) {
|
|
|
167
168
|
runtime: cfg.runtime, model: cfg.model ?? "(runtime default)",
|
|
168
169
|
threshold: cfg.threshold, conf: cfg.conf, librarySize: library.length,
|
|
169
170
|
cases: measured, skipped, totalCost, exitCode,
|
|
171
|
+
manifest: buildManifest("context", cfg),
|
|
170
172
|
};
|
|
171
173
|
}
|
package/dist/diagnose.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type { Config } from "./types.ts";
|
|
|
15
15
|
import { reliability } from "./stats.ts";
|
|
16
16
|
import { type RunStats } from "./orchestrator.ts";
|
|
17
17
|
import { type SkillInfo } from "./gen.ts";
|
|
18
|
+
import { type RunManifest } from "./manifest.ts";
|
|
18
19
|
export type DiagVerdict = "routes-ok" | "routing-miss" | "description-problem" | "inconclusive" | "error";
|
|
19
20
|
/** Returns the chosen skill name, "none", or null when the answer is unusable (LLM error/garbage). */
|
|
20
21
|
export type ChoiceFn = (skills: SkillInfo[], prompt: string, model?: string) => Promise<string | null>;
|
|
@@ -50,6 +51,8 @@ export interface DiagnoseResult {
|
|
|
50
51
|
skipped: string[];
|
|
51
52
|
totalCost: number;
|
|
52
53
|
exitCode: 0 | 1 | 2;
|
|
54
|
+
/** who/what/when of this run — makes reports citable and comparable */
|
|
55
|
+
manifest: RunManifest;
|
|
53
56
|
}
|
|
54
57
|
export interface DiagProgress {
|
|
55
58
|
caseIndex: number;
|
package/dist/diagnose.js
CHANGED
|
@@ -4,6 +4,7 @@ import { getAdapter } from "./adapters/index.js";
|
|
|
4
4
|
import { listSkills } from "./gen.js";
|
|
5
5
|
import { complete } from "./llm.js";
|
|
6
6
|
import { ConfigError } from "./config.js";
|
|
7
|
+
import { buildManifest } from "./manifest.js";
|
|
7
8
|
const ROUTER_SYSTEM = "You are a skill router. Given a user request and a list of available skills (name + " +
|
|
8
9
|
"description), decide which single skill should handle the request. Reply with EXACTLY one skill " +
|
|
9
10
|
"name from the list, or the word none if no skill fits. Output only that one word — no punctuation, " +
|
|
@@ -150,5 +151,6 @@ export async function runDiagnose(cfg, deps = {}) {
|
|
|
150
151
|
return {
|
|
151
152
|
runtime: cfg.runtime, model: cfg.model ?? "(runtime default)",
|
|
152
153
|
threshold: cfg.threshold, conf: cfg.conf, cases, skipped, totalCost, exitCode,
|
|
154
|
+
manifest: buildManifest("diagnose", cfg),
|
|
153
155
|
};
|
|
154
156
|
}
|
package/dist/eval.d.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import type { Config } from "./types.ts";
|
|
4
4
|
import { reliability } from "./stats.ts";
|
|
5
5
|
import { type RunStats } from "./orchestrator.ts";
|
|
6
|
+
import { type RunManifest } from "./manifest.ts";
|
|
6
7
|
/** Four honest states. `error` = the runtime failed enough that the measurement is untrustworthy. */
|
|
7
8
|
export type Verdict = "pass" | "fail" | "inconclusive" | "error";
|
|
8
9
|
export interface CaseResult {
|
|
@@ -29,6 +30,8 @@ export interface AuditResult {
|
|
|
29
30
|
};
|
|
30
31
|
/** 0 = all pass; 1 = a behavioral failure/theft; 2 = inconclusive or infrastructure error */
|
|
31
32
|
exitCode: 0 | 1 | 2;
|
|
33
|
+
/** who/what/when of this run — makes reports citable and baselines comparable */
|
|
34
|
+
manifest: RunManifest;
|
|
32
35
|
}
|
|
33
36
|
export interface ProgressEvent {
|
|
34
37
|
caseIndex: number;
|
package/dist/eval.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { reliability, sequentialDecision } from "./stats.js";
|
|
2
2
|
import { measure } from "./orchestrator.js";
|
|
3
3
|
import { getAdapter } from "./adapters/index.js";
|
|
4
|
+
import { buildManifest } from "./manifest.js";
|
|
4
5
|
function verdictFor(stats, decision, theft) {
|
|
5
6
|
// `error` means INFRASTRUCTURE failures dominate — untrustworthy. It is NOT about small k:
|
|
6
7
|
// a small-but-all-valid run is `inconclusive` (wide CI), which the report explains.
|
|
@@ -39,9 +40,9 @@ export async function runAudit(cfg, onProgress) {
|
|
|
39
40
|
theft, decision, verdict: verdictFor(stats, decision, theft),
|
|
40
41
|
});
|
|
41
42
|
}
|
|
42
|
-
// NOTE:
|
|
43
|
-
//
|
|
44
|
-
//
|
|
43
|
+
// NOTE: per-case verdicts here are CI decisions vs the threshold, not p-values — so there is no
|
|
44
|
+
// family to BH-correct in this path. Where families of Fisher tests exist (context, ablation,
|
|
45
|
+
// baseline comparison) the correction is applied.
|
|
45
46
|
const counts = {
|
|
46
47
|
pass: cases.filter((c) => c.verdict === "pass").length,
|
|
47
48
|
fail: cases.filter((c) => c.verdict === "fail").length,
|
|
@@ -52,5 +53,6 @@ export async function runAudit(cfg, onProgress) {
|
|
|
52
53
|
return {
|
|
53
54
|
runtime: cfg.runtime, model: cfg.model ?? "(runtime default)",
|
|
54
55
|
threshold: cfg.threshold, cases, totalCost, counts, exitCode,
|
|
56
|
+
manifest: buildManifest("audit", cfg),
|
|
55
57
|
};
|
|
56
58
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Config } from "./types.ts";
|
|
2
|
+
export interface RunManifest {
|
|
3
|
+
tool: "skill-probe";
|
|
4
|
+
version: string;
|
|
5
|
+
command: "audit" | "context" | "diagnose";
|
|
6
|
+
/** ISO 8601 UTC */
|
|
7
|
+
date: string;
|
|
8
|
+
runtime: string;
|
|
9
|
+
model: string;
|
|
10
|
+
k: number;
|
|
11
|
+
threshold: number;
|
|
12
|
+
conf: number;
|
|
13
|
+
/** sha256 (first 16 hex chars) of the resolved config — cases + settings, NOT cwd */
|
|
14
|
+
configHash: string;
|
|
15
|
+
node: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function toolVersion(): string;
|
|
18
|
+
/** Hash the parts of the config that change what was measured. `cwd` is deliberately EXCLUDED:
|
|
19
|
+
* the same library checked out at a different path (CI runner vs laptop) is the same experiment. */
|
|
20
|
+
export declare function configHash(cfg: Config): string;
|
|
21
|
+
export declare function buildManifest(command: RunManifest["command"], cfg: Config): RunManifest;
|
package/dist/manifest.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** Run manifest — stamps every probe-running result so two runs are comparable and a report is
|
|
2
|
+
* citable: tool version, command, when it ran, runtime/model, the statistical settings, and a
|
|
3
|
+
* hash of the resolved config. Same configHash = same cases + settings = apples to apples; the
|
|
4
|
+
* baseline comparison uses it to warn when a gate compares different experiments. */
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
let cachedVersion;
|
|
9
|
+
export function toolVersion() {
|
|
10
|
+
if (cachedVersion === undefined) {
|
|
11
|
+
try {
|
|
12
|
+
const pkg = JSON.parse(readFileSync(join(import.meta.dirname, "..", "package.json"), "utf8"));
|
|
13
|
+
cachedVersion = pkg.version ?? "unknown";
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
cachedVersion = "unknown";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return cachedVersion;
|
|
20
|
+
}
|
|
21
|
+
/** Hash the parts of the config that change what was measured. `cwd` is deliberately EXCLUDED:
|
|
22
|
+
* the same library checked out at a different path (CI runner vs laptop) is the same experiment. */
|
|
23
|
+
export function configHash(cfg) {
|
|
24
|
+
const canonical = JSON.stringify({
|
|
25
|
+
runtime: cfg.runtime,
|
|
26
|
+
model: cfg.model ?? null,
|
|
27
|
+
k: cfg.k,
|
|
28
|
+
threshold: cfg.threshold,
|
|
29
|
+
conf: cfg.conf,
|
|
30
|
+
cases: cfg.cases.map((c) => ({ prompt: c.prompt, expected: c.expected })),
|
|
31
|
+
});
|
|
32
|
+
return createHash("sha256").update(canonical).digest("hex").slice(0, 16);
|
|
33
|
+
}
|
|
34
|
+
export function buildManifest(command, cfg) {
|
|
35
|
+
return {
|
|
36
|
+
tool: "skill-probe",
|
|
37
|
+
version: toolVersion(),
|
|
38
|
+
command,
|
|
39
|
+
date: new Date().toISOString(),
|
|
40
|
+
runtime: cfg.runtime,
|
|
41
|
+
model: cfg.model ?? "(runtime default)",
|
|
42
|
+
k: cfg.k,
|
|
43
|
+
threshold: cfg.threshold,
|
|
44
|
+
conf: cfg.conf,
|
|
45
|
+
configHash: configHash(cfg),
|
|
46
|
+
node: process.version,
|
|
47
|
+
};
|
|
48
|
+
}
|
package/dist/report.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { FixResult } from "./fix.ts";
|
|
|
3
3
|
import type { ContextAuditResult } from "./context.ts";
|
|
4
4
|
import type { DoctorResult } from "./doctor.ts";
|
|
5
5
|
import type { DiagnoseResult } from "./diagnose.ts";
|
|
6
|
+
import type { BaselineComparison } from "./baseline.ts";
|
|
6
7
|
/** Smallest k at which an all-success run could certify reliability >= threshold. */
|
|
7
8
|
export declare function kToCertify(threshold: number, conf: number): number;
|
|
8
9
|
export interface RenderOpts {
|
|
@@ -17,4 +18,6 @@ export declare function renderDiagnose(d: DiagnoseResult, opts?: RenderOpts): st
|
|
|
17
18
|
export declare function renderDoctor(d: DoctorResult): string;
|
|
18
19
|
/** activation-rate-by-context: isolation vs co-loaded, with a Fisher's-exact p on the drop. */
|
|
19
20
|
export declare function renderContext(a: ContextAuditResult, opts?: RenderOpts): string;
|
|
21
|
+
/** regression gate vs a saved baseline: only significant (BH-corrected) drops fail the build. */
|
|
22
|
+
export declare function renderBaseline(cmp: BaselineComparison): string;
|
|
20
23
|
export declare function renderFix(f: FixResult): string;
|
package/dist/report.js
CHANGED
|
@@ -201,6 +201,39 @@ export function renderContext(a, opts = {}) {
|
|
|
201
201
|
(showCost ? ` | cost $${a.totalCost.toFixed(4)}` : ""));
|
|
202
202
|
return out.join("\n");
|
|
203
203
|
}
|
|
204
|
+
/** regression gate vs a saved baseline: only significant (BH-corrected) drops fail the build. */
|
|
205
|
+
export function renderBaseline(cmp) {
|
|
206
|
+
const out = [];
|
|
207
|
+
out.push(`Baseline gate — vs baseline saved ${cmp.baselineDate} (skill-probe ${cmp.baselineVersion})`);
|
|
208
|
+
for (const m of cmp.mismatches)
|
|
209
|
+
out.push(` ⚠ ${m}`);
|
|
210
|
+
for (const r of cmp.cases) {
|
|
211
|
+
if (r.untrustworthy) {
|
|
212
|
+
out.push(` [⛔ NOT COMPARABLE] ${r.expected ?? "(decoy)"} | ${r.prompt} — a side has no valid probes`);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (!r.regressed && !r.improved)
|
|
216
|
+
continue; // stable cases are summarized, not listed
|
|
217
|
+
const flag = r.regressed ? "▼ REGRESSED" : "▲ improved";
|
|
218
|
+
out.push(` [${flag}] ${r.expected ?? "(decoy)"} | ${r.prompt}`);
|
|
219
|
+
out.push(` ${pct(r.before.pHat)} (${r.before.hits}/${r.before.n}) → ${pct(r.after.pHat)} ` +
|
|
220
|
+
`(${r.after.hits}/${r.after.n}) Δ${r.delta >= 0 ? "+" : ""}${pct(r.delta)} ` +
|
|
221
|
+
`p=${pval(r.fisherP)} · adj ${pval(r.fisherPAdj)}`);
|
|
222
|
+
}
|
|
223
|
+
const extras = [];
|
|
224
|
+
if (cmp.newCases.length)
|
|
225
|
+
extras.push(`${cmp.newCases.length} new case(s) not in baseline (measured, not gated)`);
|
|
226
|
+
if (cmp.missingCases.length)
|
|
227
|
+
extras.push(`${cmp.missingCases.length} baseline case(s) missing from this run`);
|
|
228
|
+
if (extras.length)
|
|
229
|
+
out.push(` ${extras.join(" · ")}`);
|
|
230
|
+
out.push(cmp.regressions
|
|
231
|
+
? `Gate: ▼ FAIL — ${cmp.regressions} significant regression(s)` +
|
|
232
|
+
(cmp.improvements ? `, ${cmp.improvements} improvement(s)` : "")
|
|
233
|
+
: `Gate: ✅ no significant regressions (${cmp.cases.length} gated` +
|
|
234
|
+
(cmp.improvements ? `, ${cmp.improvements} improved — consider re-saving the baseline` : "") + `)`);
|
|
235
|
+
return out.join("\n");
|
|
236
|
+
}
|
|
204
237
|
export function renderFix(f) {
|
|
205
238
|
const out = [];
|
|
206
239
|
out.push(`skill-probe fix — skill: ${f.skill}`);
|
package/package.json
CHANGED