@skill-harness/core 0.16.0 → 0.18.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 +1 -2
- package/dist/adapters/types.d.ts +0 -12
- package/dist/capture-trace-types.d.ts +0 -96
- package/dist/capture-trace-types.js +0 -4
- package/dist/command-cost.d.ts +1 -1
- package/dist/command-cost.js +0 -5
- package/dist/execution-trace.js +1 -1
- package/dist/index.d.ts +3 -23
- package/dist/index.js +3 -23
- package/dist/instruction-coverage.d.ts +0 -7
- package/dist/instruction-coverage.js +5 -7
- package/dist/journal.d.ts +0 -8
- package/dist/metrics.d.ts +18 -0
- package/dist/metrics.js +28 -0
- package/dist/redaction.d.ts +6 -0
- package/dist/redaction.js +55 -0
- package/dist/regate.js +6 -71
- package/dist/regrade.d.ts +1 -3
- package/dist/regrade.js +1 -21
- package/dist/report.d.ts +1 -15
- package/dist/report.js +1 -13
- package/dist/reps.d.ts +1 -2
- package/dist/reps.js +0 -2
- package/dist/results.d.ts +0 -9
- package/dist/results.js +2 -19
- package/dist/run.js +11 -72
- package/dist/seeded.d.ts +1 -7
- package/dist/seeded.js +5 -11
- package/dist/sources.js +4 -9
- package/dist/spec.d.ts +0 -13
- package/dist/spec.js +6 -48
- package/dist/trajectory-events.d.ts +58 -0
- package/dist/trajectory-events.js +119 -0
- package/package.json +1 -1
- package/schemas/results-v2.schema.json +376 -75
- package/schemas/specification-v1.schema.json +128 -174
- package/dist/adjudication.d.ts +0 -219
- package/dist/adjudication.js +0 -422
- package/dist/adoption.d.ts +0 -128
- package/dist/adoption.js +0 -102
- package/dist/affected.d.ts +0 -88
- package/dist/affected.js +0 -222
- package/dist/capture.d.ts +0 -193
- package/dist/capture.js +0 -344
- package/dist/comparison.d.ts +0 -97
- package/dist/comparison.js +0 -189
- package/dist/factory-calibration.d.ts +0 -63
- package/dist/factory-calibration.js +0 -115
- package/dist/intervention-results.d.ts +0 -6
- package/dist/intervention-results.js +0 -52
- package/dist/intervention.d.ts +0 -123
- package/dist/intervention.js +0 -218
- package/dist/investigation.d.ts +0 -94
- package/dist/investigation.js +0 -148
- package/dist/judge-agreement.d.ts +0 -19
- package/dist/judge-agreement.js +0 -31
- package/dist/qualification-capture.d.ts +0 -44
- package/dist/qualification-capture.js +0 -163
- package/dist/qualification-config.d.ts +0 -233
- package/dist/qualification-config.js +0 -694
- package/dist/qualification-lock.d.ts +0 -15
- package/dist/qualification-lock.js +0 -87
- package/dist/qualification-oauth-directory.d.ts +0 -79
- package/dist/qualification-oauth-directory.js +0 -444
- package/dist/qualification-panel-store.d.ts +0 -14
- package/dist/qualification-panel-store.js +0 -188
- package/dist/qualification-panels.d.ts +0 -99
- package/dist/qualification-panels.js +0 -91
- package/dist/qualification-process.d.ts +0 -13
- package/dist/qualification-process.js +0 -190
- package/dist/qualification-runner.d.ts +0 -203
- package/dist/qualification-runner.js +0 -2037
- package/dist/qualification-store.d.ts +0 -187
- package/dist/qualification-store.js +0 -833
- package/dist/qualification-validation.d.ts +0 -12
- package/dist/qualification-validation.js +0 -8
- package/dist/screen.d.ts +0 -31
- package/dist/screen.js +0 -105
- package/dist/trajectory-gates.d.ts +0 -137
- package/dist/trajectory-gates.js +0 -576
- package/dist/trusted-host-supervision.d.ts +0 -25
- package/dist/trusted-host-supervision.js +0 -94
- package/dist/work-capture-schema.d.ts +0 -38
- package/dist/work-capture-schema.js +0 -33
- package/dist/work-capture.d.ts +0 -43
- package/dist/work-capture.js +0 -81
- package/dist/work-signals.d.ts +0 -55
- package/dist/work-signals.js +0 -71
- package/schemas/qualification-cell-result-v1.schema.json +0 -32
- package/schemas/qualification-config-v1.schema.json +0 -244
- package/schemas/qualification-invocation-request-v1.schema.json +0 -59
- package/schemas/qualification-judge-panel-result-v1.schema.json +0 -42
- package/schemas/trajectory-event-v1.schema.json +0 -80
package/dist/capture.js
DELETED
|
@@ -1,344 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { sep } from "node:path";
|
|
3
|
-
import { CAPTURE_SCHEMA_VERSION } from "./capture-trace-types.js";
|
|
4
|
-
/**
|
|
5
|
-
* The chain of entries ending at `leafId`, in root→leaf order.
|
|
6
|
-
*
|
|
7
|
-
* With no `leafId`, the leaf is the last entry in file order — which is what
|
|
8
|
-
* "the conversation I am currently in" means. Entries with no `id` (the
|
|
9
|
-
* `session` header) are not part of the chain and are dropped.
|
|
10
|
-
*
|
|
11
|
-
* Cycles cannot occur in a well-formed file, but a corrupted one must not hang
|
|
12
|
-
* the agent, so the walk is bounded by the entry count.
|
|
13
|
-
*/
|
|
14
|
-
export function activeBranch(entries, leafId) {
|
|
15
|
-
const byId = new Map();
|
|
16
|
-
for (const e of entries)
|
|
17
|
-
if (typeof e.id === "string")
|
|
18
|
-
byId.set(e.id, e);
|
|
19
|
-
let cursor = leafId;
|
|
20
|
-
if (cursor === undefined) {
|
|
21
|
-
for (let i = entries.length - 1; i >= 0; i--) {
|
|
22
|
-
if (typeof entries[i].id === "string") {
|
|
23
|
-
cursor = entries[i].id;
|
|
24
|
-
break;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
const chain = [];
|
|
29
|
-
const seen = new Set();
|
|
30
|
-
while (cursor !== undefined && cursor !== null && byId.has(cursor) && chain.length <= entries.length) {
|
|
31
|
-
if (seen.has(cursor))
|
|
32
|
-
break;
|
|
33
|
-
seen.add(cursor);
|
|
34
|
-
const entry = byId.get(cursor);
|
|
35
|
-
chain.push(entry);
|
|
36
|
-
cursor = entry.parentId ?? undefined;
|
|
37
|
-
}
|
|
38
|
-
return chain.reverse();
|
|
39
|
-
}
|
|
40
|
-
/** Extract visible text from content blocks. Thinking is dropped unconditionally. */
|
|
41
|
-
export function visibleText(blocks) {
|
|
42
|
-
if (!blocks)
|
|
43
|
-
return "";
|
|
44
|
-
const parts = [];
|
|
45
|
-
for (const b of blocks) {
|
|
46
|
-
if (b.type === "thinking")
|
|
47
|
-
continue; // never, at any call site
|
|
48
|
-
if (b.type === "text" && typeof b.text === "string")
|
|
49
|
-
parts.push(b.text);
|
|
50
|
-
else if (b.type === "image")
|
|
51
|
-
parts.push("[image omitted]");
|
|
52
|
-
}
|
|
53
|
-
return parts.join("\n").trim();
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Group an already-branch-resolved entry list into logical turns.
|
|
57
|
-
*
|
|
58
|
-
* Entries that are not messages (`model_change`, `thinking_level_change`,
|
|
59
|
-
* compaction markers, custom extension entries) are skipped rather than
|
|
60
|
-
* guessed at: an unknown entry type is not evidence about the conversation.
|
|
61
|
-
* Anything before the first user message is dropped — it belongs to no turn.
|
|
62
|
-
*/
|
|
63
|
-
export function projectTurns(entries, homeDir) {
|
|
64
|
-
const turns = [];
|
|
65
|
-
let current = null;
|
|
66
|
-
for (const entry of entries) {
|
|
67
|
-
if (entry.type !== "message" || !entry.message)
|
|
68
|
-
continue;
|
|
69
|
-
const msg = entry.message;
|
|
70
|
-
const id = typeof entry.id === "string" ? entry.id : "";
|
|
71
|
-
if (msg.role === "user") {
|
|
72
|
-
current = {
|
|
73
|
-
index: turns.length,
|
|
74
|
-
user: visibleText(msg.content),
|
|
75
|
-
assistantText: "",
|
|
76
|
-
toolCalls: [],
|
|
77
|
-
entryIds: id ? [id] : [],
|
|
78
|
-
};
|
|
79
|
-
turns.push(current);
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
82
|
-
if (!current)
|
|
83
|
-
continue; // pre-conversation noise belongs to no turn
|
|
84
|
-
if (id)
|
|
85
|
-
current.entryIds.push(id);
|
|
86
|
-
if (msg.role === "assistant") {
|
|
87
|
-
const text = visibleText(msg.content);
|
|
88
|
-
if (text)
|
|
89
|
-
current.assistantText = current.assistantText ? `${current.assistantText}\n${text}` : text;
|
|
90
|
-
for (const b of msg.content ?? []) {
|
|
91
|
-
if (b.type !== "toolCall")
|
|
92
|
-
continue;
|
|
93
|
-
current.toolCalls.push({
|
|
94
|
-
name: typeof b.name === "string" ? b.name : "(unknown)",
|
|
95
|
-
// Without `homeDir` this scrubbed secrets but left absolute home paths
|
|
96
|
-
// intact — and these args are what the evidence sidecar records.
|
|
97
|
-
args: redactArgs(b.arguments, homeDir),
|
|
98
|
-
isError: false,
|
|
99
|
-
...(typeof b.id === "string" ? { id: b.id } : {}),
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
continue;
|
|
103
|
-
}
|
|
104
|
-
if (msg.role === "toolResult") {
|
|
105
|
-
// Attach the outcome to the matching call. Result BODIES are never kept —
|
|
106
|
-
// they routinely carry file contents, command output and absolute paths.
|
|
107
|
-
const body = JSON.stringify(msg.content ?? []);
|
|
108
|
-
const callId = typeof msg.toolCallId === "string" ? msg.toolCallId : undefined;
|
|
109
|
-
// By id when pi gave us one; otherwise the FIRST still-unmatched call of
|
|
110
|
-
// that name, since results arrive in order for a sequential conversation.
|
|
111
|
-
const target = callId
|
|
112
|
-
? current.toolCalls.find((c) => c.id === callId)
|
|
113
|
-
: current.toolCalls.find((c) => c.name === msg.toolName && c.resultBytes === undefined);
|
|
114
|
-
if (target) {
|
|
115
|
-
target.isError = msg.isError === true;
|
|
116
|
-
target.resultBytes = Buffer.byteLength(body, "utf8");
|
|
117
|
-
target.resultSha256 = sha256(body);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return turns;
|
|
122
|
-
}
|
|
123
|
-
// ---------------------------------------------------------------------------
|
|
124
|
-
// Redaction
|
|
125
|
-
// ---------------------------------------------------------------------------
|
|
126
|
-
/** Values longer than this are truncated rather than persisted whole. */
|
|
127
|
-
export const MAX_VALUE_CHARS = 2000;
|
|
128
|
-
const REDACTED = "[redacted]";
|
|
129
|
-
/** Argument/field names whose VALUE is always dropped, whatever it looks like. */
|
|
130
|
-
const SECRET_KEY = /^(.*[-_])?(password|passwd|secret|token|api[-_]?key|apikey|auth|authorization|credential|private[-_]?key|access[-_]?key|session[-_]?key)([-_].*)?$/i;
|
|
131
|
-
/** Value-shaped secrets, caught even under an innocuous key name. */
|
|
132
|
-
const SECRET_VALUE = [
|
|
133
|
-
/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
|
|
134
|
-
/\bBearer\s+[A-Za-z0-9._~+/-]{16,}=*/g,
|
|
135
|
-
/\bsk-[A-Za-z0-9]{16,}\b/g,
|
|
136
|
-
/\bgh[pousr]_[A-Za-z0-9]{16,}\b/g,
|
|
137
|
-
/\bxox[abposr]-[A-Za-z0-9-]{10,}\b/g,
|
|
138
|
-
/\bAKIA[0-9A-Z]{16}\b/g,
|
|
139
|
-
/\bey[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g, // JWT
|
|
140
|
-
// Credentials embedded in a URL — `postgres://user:pass@host/db`. Caught by
|
|
141
|
-
// shape rather than by key name: the key is usually something like `DB_URL`,
|
|
142
|
-
// which no list of secret-sounding names will ever match.
|
|
143
|
-
/\b([a-z][a-z0-9+.-]*:\/\/)[^/\s:@]+:[^/\s@]+@/gi,
|
|
144
|
-
];
|
|
145
|
-
/**
|
|
146
|
-
* Redact secrets and machine paths from a free-text string.
|
|
147
|
-
*
|
|
148
|
-
* Home directories are replaced because a capture is meant to be committed, and
|
|
149
|
-
* `/home/<name>/…` identifies a person as surely as a token identifies an
|
|
150
|
-
* account. `homeDir` is a parameter rather than a read of `process.env.HOME` so
|
|
151
|
-
* the behavior is testable and does not differ between a developer's machine
|
|
152
|
-
* and CI.
|
|
153
|
-
*/
|
|
154
|
-
export function redactText(input, homeDir) {
|
|
155
|
-
let out = input;
|
|
156
|
-
// The URL pattern keeps its scheme so the value stays recognisable as a URL;
|
|
157
|
-
// every other pattern replaces the whole match.
|
|
158
|
-
out = out.replace(SECRET_VALUE[SECRET_VALUE.length - 1], `$1${REDACTED}@`);
|
|
159
|
-
for (const re of SECRET_VALUE.slice(0, -1))
|
|
160
|
-
out = out.replace(re, REDACTED);
|
|
161
|
-
if (homeDir && homeDir.length > 1) {
|
|
162
|
-
out = out.split(homeDir).join("~");
|
|
163
|
-
}
|
|
164
|
-
return out;
|
|
165
|
-
}
|
|
166
|
-
/** Truncate with an explicit marker — silent truncation reads as complete evidence. */
|
|
167
|
-
export function truncate(input, max = MAX_VALUE_CHARS) {
|
|
168
|
-
if (input.length <= max)
|
|
169
|
-
return input;
|
|
170
|
-
return `${input.slice(0, max)}… [truncated ${input.length - max} chars]`;
|
|
171
|
-
}
|
|
172
|
-
/**
|
|
173
|
-
* Sanitize tool-call arguments: drop secret-named values, redact secret-shaped
|
|
174
|
-
* ones, truncate the oversized, and refuse to recurse without bound.
|
|
175
|
-
*/
|
|
176
|
-
export function redactArgs(args, homeDir, depth = 0) {
|
|
177
|
-
if (args === null || typeof args !== "object" || Array.isArray(args))
|
|
178
|
-
return {};
|
|
179
|
-
const out = {};
|
|
180
|
-
for (const [key, value] of Object.entries(args)) {
|
|
181
|
-
if (SECRET_KEY.test(key)) {
|
|
182
|
-
out[key] = REDACTED;
|
|
183
|
-
continue;
|
|
184
|
-
}
|
|
185
|
-
out[key] = redactValue(value, homeDir, depth);
|
|
186
|
-
}
|
|
187
|
-
return out;
|
|
188
|
-
}
|
|
189
|
-
function redactValue(value, homeDir, depth) {
|
|
190
|
-
if (typeof value === "string")
|
|
191
|
-
return truncate(redactText(value, homeDir));
|
|
192
|
-
if (typeof value === "number" || typeof value === "boolean" || value === null)
|
|
193
|
-
return value;
|
|
194
|
-
if (depth >= 3)
|
|
195
|
-
return "[nested]"; // bounded: a deep object is not review evidence
|
|
196
|
-
if (Array.isArray(value))
|
|
197
|
-
return value.slice(0, 20).map((v) => redactValue(v, homeDir, depth + 1));
|
|
198
|
-
if (typeof value === "object")
|
|
199
|
-
return redactArgs(value, homeDir, depth + 1);
|
|
200
|
-
return String(value);
|
|
201
|
-
}
|
|
202
|
-
function sha256(text) {
|
|
203
|
-
return createHash("sha256").update(text, "utf8").digest("hex");
|
|
204
|
-
}
|
|
205
|
-
// ---------------------------------------------------------------------------
|
|
206
|
-
// Building a capture case
|
|
207
|
-
// ---------------------------------------------------------------------------
|
|
208
|
-
/** Stable, sortable, collision-resistant enough for a per-skill directory. */
|
|
209
|
-
export function captureId(seed, existing = []) {
|
|
210
|
-
const taken = new Set(existing);
|
|
211
|
-
const base = `CAP-${sha256(seed).slice(0, 6).toUpperCase()}`;
|
|
212
|
-
if (!taken.has(base))
|
|
213
|
-
return base;
|
|
214
|
-
for (let n = 2;; n++) {
|
|
215
|
-
const candidate = `${base}-${n}`;
|
|
216
|
-
if (!taken.has(candidate))
|
|
217
|
-
return candidate;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
/**
|
|
221
|
-
* Assemble a reviewed capture case.
|
|
222
|
-
*
|
|
223
|
-
* Only the USER turns are carried into the case: they are the stimulus, and they
|
|
224
|
-
* are the only part that can be replayed. The assistant's historical prose is
|
|
225
|
-
* evidence for the human writing the expectation, never an exact-output oracle —
|
|
226
|
-
* so it lives in the git-ignored local sidecar, not here.
|
|
227
|
-
*
|
|
228
|
-
* The session path is hashed rather than stored: an absolute path names a
|
|
229
|
-
* machine and a user, and a hash is enough to recognize the same session again.
|
|
230
|
-
*/
|
|
231
|
-
export function buildCaptureCase(opts) {
|
|
232
|
-
const { start, end } = opts.range;
|
|
233
|
-
if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end < start || end >= opts.turns.length) {
|
|
234
|
-
throw new Error(`invalid capture range ${start}..${end} over ${opts.turns.length} turn(s)`);
|
|
235
|
-
}
|
|
236
|
-
if (opts.expectedBehavior.trim() === "") {
|
|
237
|
-
throw new Error("a capture needs a written expected behavior — it is what makes the case reviewable");
|
|
238
|
-
}
|
|
239
|
-
const checklist = opts.checklist.map((c) => c.trim()).filter(Boolean);
|
|
240
|
-
if (checklist.length === 0) {
|
|
241
|
-
throw new Error("a capture needs at least one checklist item");
|
|
242
|
-
}
|
|
243
|
-
const selected = opts.turns.slice(start, end + 1);
|
|
244
|
-
const turns = selected.map((t) => truncate(redactText(t.user, opts.homeDir)));
|
|
245
|
-
const id = captureId(`${opts.sessionPath}:${start}:${end}:${opts.created}`, opts.existingIds ?? []);
|
|
246
|
-
return {
|
|
247
|
-
capture_schema: CAPTURE_SCHEMA_VERSION,
|
|
248
|
-
id,
|
|
249
|
-
created: opts.created,
|
|
250
|
-
classification: opts.classification,
|
|
251
|
-
turns,
|
|
252
|
-
expected_behavior: truncate(redactText(opts.expectedBehavior, opts.homeDir)),
|
|
253
|
-
checklist: checklist.map((c) => truncate(redactText(c, opts.homeDir), 300)),
|
|
254
|
-
target: opts.target,
|
|
255
|
-
// `covers` refs resolve against the SPEC dir, and `target.path` is relative
|
|
256
|
-
// to the skill root — one level up. A subagent path (`.pi/agents/x.md`) is
|
|
257
|
-
// carried the same way; both are instruction files a section walk can read.
|
|
258
|
-
covers: [`../${opts.target.path.split(sep).join("/")}`],
|
|
259
|
-
provenance: {
|
|
260
|
-
session_sha256: sha256(opts.sessionPath),
|
|
261
|
-
turn_range: { start, end },
|
|
262
|
-
...(opts.subject ? { subject: opts.subject } : {}),
|
|
263
|
-
...(opts.gitCommit ? { git_commit: opts.gitCommit } : {}),
|
|
264
|
-
...(opts.gitDirty === undefined ? {} : { git_dirty: opts.gitDirty }),
|
|
265
|
-
},
|
|
266
|
-
status: "pending",
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
/**
|
|
270
|
-
* Project a reviewed capture into a scenario object for `appendScenario`.
|
|
271
|
-
*
|
|
272
|
-
* Deliberately minimal: id, title, turns, checklist. No `critical`, no gates, no
|
|
273
|
-
* fixture. Promotion is the moment a human takes responsibility for a test, and
|
|
274
|
-
* a capture cannot know whether the behavior it saw is ship-blocking — guessing
|
|
275
|
-
* `critical: true` here would let a captured one-off silently gate a release.
|
|
276
|
-
*/
|
|
277
|
-
export function captureToScenario(capture, scenarioId, title) {
|
|
278
|
-
return {
|
|
279
|
-
id: scenarioId,
|
|
280
|
-
title,
|
|
281
|
-
turns: capture.turns,
|
|
282
|
-
checklist: capture.checklist,
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
/**
|
|
286
|
-
* Draft an orchestration assertion from subagent calls seen in the captured range.
|
|
287
|
-
*
|
|
288
|
-
* Returns null when the range contains none, so the capture UI can skip the
|
|
289
|
-
* question entirely rather than offering an empty form.
|
|
290
|
-
*
|
|
291
|
-
* Deliberately proposes only `agent` and `count` — never `task_contains`. The
|
|
292
|
-
* task text that happened to be sent is not the task text that is *required*;
|
|
293
|
-
* turning one observed handoff into a required substring would manufacture a
|
|
294
|
-
* brittle assertion the author never reasoned about. Required context is a
|
|
295
|
-
* judgement, so the UI asks.
|
|
296
|
-
*/
|
|
297
|
-
export function draftSubagentAssertion(turns, toolNames = ["Agent", "subagent", "task"]) {
|
|
298
|
-
const names = new Set(toolNames.map((n) => n.toLowerCase()));
|
|
299
|
-
for (const turn of turns) {
|
|
300
|
-
for (const call of turn.toolCalls) {
|
|
301
|
-
if (!names.has(call.name.toLowerCase()))
|
|
302
|
-
continue;
|
|
303
|
-
const invocations = subagentInvocationsOf(call.args);
|
|
304
|
-
if (invocations.length === 0)
|
|
305
|
-
continue;
|
|
306
|
-
return { tool: call.name, agent: invocations[0], count: { min: 1 } };
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
return null;
|
|
310
|
-
}
|
|
311
|
-
/** Agent names visible in a call's arguments, across the known shapes. */
|
|
312
|
-
function subagentInvocationsOf(args) {
|
|
313
|
-
const nameOf = (v) => {
|
|
314
|
-
if (v === null || typeof v !== "object" || Array.isArray(v))
|
|
315
|
-
return null;
|
|
316
|
-
const o = v;
|
|
317
|
-
if (typeof o.agent === "string")
|
|
318
|
-
return o.agent;
|
|
319
|
-
if (typeof o.name === "string")
|
|
320
|
-
return o.name;
|
|
321
|
-
return null;
|
|
322
|
-
};
|
|
323
|
-
for (const key of ["tasks", "chain"]) {
|
|
324
|
-
const list = args[key];
|
|
325
|
-
if (Array.isArray(list))
|
|
326
|
-
return list.map(nameOf).filter((x) => x !== null);
|
|
327
|
-
}
|
|
328
|
-
const single = nameOf(args);
|
|
329
|
-
return single ? [single] : [];
|
|
330
|
-
}
|
|
331
|
-
/**
|
|
332
|
-
* Offline first-draft checklist from the human's expectation.
|
|
333
|
-
*
|
|
334
|
-
* Splits on sentence and bullet boundaries. This is a typing shortcut for the
|
|
335
|
-
* editor step, not an understanding of the text — the UI always opens the result
|
|
336
|
-
* for correction, and the plan makes LLM drafting a separate, paid, opt-in path.
|
|
337
|
-
*/
|
|
338
|
-
export function draftChecklist(expectedBehavior) {
|
|
339
|
-
return expectedBehavior
|
|
340
|
-
.split(/\n\s*[-*]\s+|\n{2,}|(?<=[.!?])\s+(?=[A-Z])/)
|
|
341
|
-
.map((s) => s.replace(/^[-*]\s+/, "").replace(/\s+/g, " ").trim().replace(/[.]$/, ""))
|
|
342
|
-
.filter((s) => s.length > 3);
|
|
343
|
-
}
|
|
344
|
-
//# sourceMappingURL=capture.js.map
|
package/dist/comparison.d.ts
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import type { ResultsFile, ScenarioResult } from "./results.js";
|
|
2
|
-
import type { Verdict } from "./score.js";
|
|
3
|
-
export interface ComparisonSideDigests {
|
|
4
|
-
skill: string;
|
|
5
|
-
spec: string;
|
|
6
|
-
fixtures: Record<string, string>;
|
|
7
|
-
/** Every non-candidate test input: scenario facets, persona, fixtures, extensions, post-tests, and system prompts. */
|
|
8
|
-
inputs: Record<string, string>;
|
|
9
|
-
}
|
|
10
|
-
export interface ComparisonDigests {
|
|
11
|
-
reference: ComparisonSideDigests;
|
|
12
|
-
candidate: ComparisonSideDigests;
|
|
13
|
-
harness: string;
|
|
14
|
-
model: string;
|
|
15
|
-
judge: string;
|
|
16
|
-
environment: {
|
|
17
|
-
node: string;
|
|
18
|
-
platform: string;
|
|
19
|
-
arch: string;
|
|
20
|
-
[key: string]: string;
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
export interface ComparisonThresholds {
|
|
24
|
-
max_subject_token_increase?: number;
|
|
25
|
-
max_wall_time_increase?: number;
|
|
26
|
-
max_tool_call_increase?: number;
|
|
27
|
-
}
|
|
28
|
-
export interface ComparisonCell {
|
|
29
|
-
id: string;
|
|
30
|
-
reference: Verdict;
|
|
31
|
-
candidate: Verdict;
|
|
32
|
-
change: "regression" | "lift" | "held-pass" | "held-fail" | "inconclusive";
|
|
33
|
-
critical: boolean;
|
|
34
|
-
reference_flakiness: number | null;
|
|
35
|
-
candidate_flakiness: number | null;
|
|
36
|
-
}
|
|
37
|
-
export interface AggregateMetrics {
|
|
38
|
-
wall_time_ms: number;
|
|
39
|
-
judge_calls: number;
|
|
40
|
-
judge_rejudge_calls: number;
|
|
41
|
-
subject_metrics_reps: number;
|
|
42
|
-
total_reps: number;
|
|
43
|
-
input_tokens: number | null;
|
|
44
|
-
output_tokens: number | null;
|
|
45
|
-
cache_read_tokens: number | null;
|
|
46
|
-
cache_write_tokens: number | null;
|
|
47
|
-
subject_cost_usd: number | null;
|
|
48
|
-
cost_source: "provider-reported" | "subscription" | "unreported" | null;
|
|
49
|
-
tool_calls: number | null;
|
|
50
|
-
delegated_children: number | null;
|
|
51
|
-
max_concurrency: number | null;
|
|
52
|
-
}
|
|
53
|
-
export interface ComparisonReport {
|
|
54
|
-
schema: 1;
|
|
55
|
-
skill: string;
|
|
56
|
-
model: string;
|
|
57
|
-
mode: string;
|
|
58
|
-
reps: number;
|
|
59
|
-
judge: string;
|
|
60
|
-
paired_setup: true;
|
|
61
|
-
deterministic_sampling: false;
|
|
62
|
-
sampling_note: string;
|
|
63
|
-
partial: boolean;
|
|
64
|
-
release_eligible: boolean;
|
|
65
|
-
ship: boolean;
|
|
66
|
-
note: string;
|
|
67
|
-
cells: ComparisonCell[];
|
|
68
|
-
behavioral_regressions: string[];
|
|
69
|
-
critical_regressions: string[];
|
|
70
|
-
infrastructure_errors: string[];
|
|
71
|
-
metrics: {
|
|
72
|
-
reference: AggregateMetrics;
|
|
73
|
-
candidate: AggregateMetrics;
|
|
74
|
-
};
|
|
75
|
-
cost_regressions: string[];
|
|
76
|
-
thresholds?: ComparisonThresholds;
|
|
77
|
-
digests: ComparisonDigests;
|
|
78
|
-
reference: ResultsFile;
|
|
79
|
-
candidate: ResultsFile;
|
|
80
|
-
}
|
|
81
|
-
export declare function buildComparison(input: {
|
|
82
|
-
skill: string;
|
|
83
|
-
model: string;
|
|
84
|
-
mode: string;
|
|
85
|
-
reps: number;
|
|
86
|
-
judge: string;
|
|
87
|
-
reference: ResultsFile;
|
|
88
|
-
candidate: ResultsFile;
|
|
89
|
-
critical: string[];
|
|
90
|
-
digests: ComparisonDigests;
|
|
91
|
-
partial?: boolean;
|
|
92
|
-
thresholds?: ComparisonThresholds;
|
|
93
|
-
}): ComparisonReport;
|
|
94
|
-
/** 2 = critical behavioral regression, 1 = ordinary/ship/infra regression, 0 = clean or partial feedback. */
|
|
95
|
-
export declare function comparisonExitCode(report: ComparisonReport): 0 | 1 | 2;
|
|
96
|
-
export declare function formatComparison(report: ComparisonReport): string;
|
|
97
|
-
export declare function aggregateMetrics(scenarios: ScenarioResult[]): AggregateMetrics;
|
package/dist/comparison.js
DELETED
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
export function buildComparison(input) {
|
|
2
|
-
const { reference, candidate } = input;
|
|
3
|
-
if (reference.model !== candidate.model || reference.model !== input.model)
|
|
4
|
-
throw new Error("comparison subject models differ");
|
|
5
|
-
if (reference.mode !== candidate.mode || reference.mode !== input.mode)
|
|
6
|
-
throw new Error("comparison delivery modes differ");
|
|
7
|
-
const referenceJudge = `${reference.judge.provider}:${reference.judge.model}`;
|
|
8
|
-
const candidateJudge = `${candidate.judge.provider}:${candidate.judge.model}`;
|
|
9
|
-
if (referenceJudge !== candidateJudge || referenceJudge !== input.judge)
|
|
10
|
-
throw new Error("comparison judge policies differ");
|
|
11
|
-
const referenceIds = reference.scenarios.map((scenario) => scenario.id);
|
|
12
|
-
const candidateIds = candidate.scenarios.map((scenario) => scenario.id);
|
|
13
|
-
if (JSON.stringify([...referenceIds].sort()) !== JSON.stringify([...candidateIds].sort())) {
|
|
14
|
-
throw new Error("comparison scenario IDs differ — paired comparisons require the same scenario plan");
|
|
15
|
-
}
|
|
16
|
-
if (input.digests.reference.spec !== input.digests.candidate.spec ||
|
|
17
|
-
JSON.stringify(sorted(input.digests.reference.fixtures)) !== JSON.stringify(sorted(input.digests.candidate.fixtures)) ||
|
|
18
|
-
JSON.stringify(sorted(input.digests.reference.inputs)) !== JSON.stringify(sorted(input.digests.candidate.inputs))) {
|
|
19
|
-
throw new Error("comparison spec/test-input digests differ — only the skill candidate may change in a paired run");
|
|
20
|
-
}
|
|
21
|
-
const referencePlan = Object.fromEntries(reference.scenarios.map((scenario) => [scenario.id, [scenario.reps ?? 1, scenario.pass_threshold ?? null]]));
|
|
22
|
-
const candidatePlan = Object.fromEntries(candidate.scenarios.map((scenario) => [scenario.id, [scenario.reps ?? 1, scenario.pass_threshold ?? null]]));
|
|
23
|
-
if (JSON.stringify(sorted(referencePlan)) !== JSON.stringify(sorted(candidatePlan))) {
|
|
24
|
-
throw new Error("comparison repetition/threshold plans differ");
|
|
25
|
-
}
|
|
26
|
-
const critical = new Set(input.critical);
|
|
27
|
-
const byCandidate = new Map(candidate.scenarios.map((scenario) => [scenario.id, scenario]));
|
|
28
|
-
const cells = reference.scenarios.map((left) => {
|
|
29
|
-
const right = byCandidate.get(left.id);
|
|
30
|
-
const a = effective(left), b = effective(right);
|
|
31
|
-
const conclusive = !untrustworthy(left, a) && !untrustworthy(right, b);
|
|
32
|
-
const change = !conclusive
|
|
33
|
-
? "inconclusive"
|
|
34
|
-
: a === "PASS" && b !== "PASS"
|
|
35
|
-
? "regression"
|
|
36
|
-
: a !== "PASS" && b === "PASS"
|
|
37
|
-
? "lift"
|
|
38
|
-
: a === "PASS"
|
|
39
|
-
? "held-pass"
|
|
40
|
-
: "held-fail";
|
|
41
|
-
return {
|
|
42
|
-
id: left.id,
|
|
43
|
-
reference: a,
|
|
44
|
-
candidate: b,
|
|
45
|
-
change,
|
|
46
|
-
critical: critical.has(left.id),
|
|
47
|
-
reference_flakiness: left.flakiness ?? null,
|
|
48
|
-
candidate_flakiness: right.flakiness ?? null,
|
|
49
|
-
};
|
|
50
|
-
});
|
|
51
|
-
const behavioral = cells.filter((cell) => cell.change === "regression").map((cell) => cell.id);
|
|
52
|
-
const criticalRegressions = cells.filter((cell) => cell.change === "regression" && cell.critical).map((cell) => cell.id);
|
|
53
|
-
const byReference = new Map(reference.scenarios.map((scenario) => [scenario.id, scenario]));
|
|
54
|
-
const infrastructureErrors = cells.filter((cell) => cell.reference === "ERROR" || cell.reference === "NOT-MEASURED" || cell.reference === "JUDGE-AMBIGUOUS" || byReference.get(cell.id)?.suspect ||
|
|
55
|
-
cell.candidate === "ERROR" || cell.candidate === "NOT-MEASURED" || cell.candidate === "JUDGE-AMBIGUOUS" || byCandidate.get(cell.id)?.suspect).map((cell) => cell.id);
|
|
56
|
-
const metrics = { reference: aggregateMetrics(reference.scenarios), candidate: aggregateMetrics(candidate.scenarios) };
|
|
57
|
-
const costRegressions = thresholdFindings(metrics.reference, metrics.candidate, input.thresholds);
|
|
58
|
-
const partial = input.partial === true || reference.partial === true || candidate.partial === true;
|
|
59
|
-
const releaseEligible = !partial;
|
|
60
|
-
const ship = releaseEligible && candidate.effective_grade.ship && behavioral.length === 0 && infrastructureErrors.length === 0 && costRegressions.length === 0;
|
|
61
|
-
const note = partial
|
|
62
|
-
? "branch feedback only — partial/affected comparisons never SHIP; run the full paired plan for a release decision"
|
|
63
|
-
: ship
|
|
64
|
-
? "candidate clears its ship bar with no paired behavioral regression"
|
|
65
|
-
: "candidate is NOT READY under the paired behavioral/cost release policy";
|
|
66
|
-
return {
|
|
67
|
-
schema: 1,
|
|
68
|
-
skill: input.skill,
|
|
69
|
-
model: input.model,
|
|
70
|
-
mode: input.mode,
|
|
71
|
-
reps: input.reps,
|
|
72
|
-
judge: input.judge,
|
|
73
|
-
paired_setup: true,
|
|
74
|
-
deterministic_sampling: false,
|
|
75
|
-
sampling_note: "paired setup only: prompts/fixtures/models/policy/repetition plan match; provider-deterministic seeding is not claimed",
|
|
76
|
-
partial,
|
|
77
|
-
release_eligible: releaseEligible,
|
|
78
|
-
ship,
|
|
79
|
-
note,
|
|
80
|
-
cells,
|
|
81
|
-
behavioral_regressions: behavioral,
|
|
82
|
-
critical_regressions: criticalRegressions,
|
|
83
|
-
infrastructure_errors: [...new Set(infrastructureErrors)],
|
|
84
|
-
metrics,
|
|
85
|
-
cost_regressions: costRegressions,
|
|
86
|
-
...(input.thresholds ? { thresholds: input.thresholds } : {}),
|
|
87
|
-
digests: input.digests,
|
|
88
|
-
reference,
|
|
89
|
-
candidate,
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
/** 2 = critical behavioral regression, 1 = ordinary/ship/infra regression, 0 = clean or partial feedback. */
|
|
93
|
-
export function comparisonExitCode(report) {
|
|
94
|
-
if (report.critical_regressions.length)
|
|
95
|
-
return 2;
|
|
96
|
-
if (report.behavioral_regressions.length || report.infrastructure_errors.length || report.cost_regressions.length || (report.release_eligible && !report.ship))
|
|
97
|
-
return 1;
|
|
98
|
-
return 0;
|
|
99
|
-
}
|
|
100
|
-
export function formatComparison(report) {
|
|
101
|
-
const lines = [
|
|
102
|
-
`── compare ${report.skill} · ${report.model} · mode=${report.mode} ×${report.reps} ──`,
|
|
103
|
-
" sampling: paired setup; provider-deterministic seeding NOT claimed",
|
|
104
|
-
];
|
|
105
|
-
for (const cell of report.cells) {
|
|
106
|
-
const mark = cell.change === "regression" ? "↓" : cell.change === "lift" ? "↑" : cell.change === "inconclusive" ? "?" : "=";
|
|
107
|
-
const variance = cell.reference_flakiness === null && cell.candidate_flakiness === null
|
|
108
|
-
? ""
|
|
109
|
-
: ` · flakiness ${cell.reference_flakiness ?? "n/a"} → ${cell.candidate_flakiness ?? "n/a"}`;
|
|
110
|
-
lines.push(` ${mark} ${cell.id}${cell.critical ? " CRITICAL" : ""}: ${cell.reference} → ${cell.candidate} (${cell.change})${variance}`);
|
|
111
|
-
}
|
|
112
|
-
lines.push(` behavior: ${report.behavioral_regressions.length} regression(s), ${report.cells.filter((cell) => cell.change === "lift").length} lift(s), ${report.infrastructure_errors.length} infrastructure/inconclusive error(s)`);
|
|
113
|
-
lines.push(` subject tokens: input ${metricDelta(report.metrics.reference.input_tokens, report.metrics.candidate.input_tokens)} · output ${metricDelta(report.metrics.reference.output_tokens, report.metrics.candidate.output_tokens)} · cache-read ${metricDelta(report.metrics.reference.cache_read_tokens, report.metrics.candidate.cache_read_tokens)} (${report.metrics.candidate.subject_metrics_reps}/${report.metrics.candidate.total_reps} candidate reps reported)`);
|
|
114
|
-
lines.push(` judge calls: ${report.metrics.reference.judge_calls} → ${report.metrics.candidate.judge_calls} (re-judge ${report.metrics.reference.judge_rejudge_calls} → ${report.metrics.candidate.judge_rejudge_calls})`);
|
|
115
|
-
lines.push(` wall time: ${report.metrics.reference.wall_time_ms}ms → ${report.metrics.candidate.wall_time_ms}ms`);
|
|
116
|
-
lines.push(` tool calls: ${metricDelta(report.metrics.reference.tool_calls, report.metrics.candidate.tool_calls)}`);
|
|
117
|
-
if (report.cost_regressions.length)
|
|
118
|
-
lines.push(` cost/latency thresholds (separate from behavior): ${report.cost_regressions.join("; ")}`);
|
|
119
|
-
lines.push(` ${report.ship ? "SHIP" : "NOT READY"}: ${report.note}`);
|
|
120
|
-
return lines.join("\n");
|
|
121
|
-
}
|
|
122
|
-
export function aggregateMetrics(scenarios) {
|
|
123
|
-
const metrics = scenarios.map((scenario) => scenario.metrics).filter((value) => value !== undefined);
|
|
124
|
-
const sumOptional = (field) => {
|
|
125
|
-
const values = metrics.map((value) => value[field]).filter((value) => typeof value === "number");
|
|
126
|
-
return values.length ? values.reduce((sum, value) => sum + value, 0) : null;
|
|
127
|
-
};
|
|
128
|
-
const maxValues = metrics.map((value) => value.max_concurrency).filter((value) => typeof value === "number");
|
|
129
|
-
return {
|
|
130
|
-
wall_time_ms: metrics.reduce((sum, value) => sum + value.wall_time_ms, 0),
|
|
131
|
-
judge_calls: metrics.reduce((sum, value) => sum + value.judge_calls, 0),
|
|
132
|
-
judge_rejudge_calls: metrics.reduce((sum, value) => sum + value.judge_rejudge_calls, 0),
|
|
133
|
-
subject_metrics_reps: metrics.reduce((sum, value) => sum + value.subject_metrics_reps, 0),
|
|
134
|
-
total_reps: metrics.reduce((sum, value) => sum + value.total_reps, 0),
|
|
135
|
-
input_tokens: sumOptional("input_tokens"),
|
|
136
|
-
output_tokens: sumOptional("output_tokens"),
|
|
137
|
-
cache_read_tokens: sumOptional("cache_read_tokens"),
|
|
138
|
-
cache_write_tokens: sumOptional("cache_write_tokens"),
|
|
139
|
-
subject_cost_usd: sumOptional("subject_cost_usd"),
|
|
140
|
-
cost_source: (() => {
|
|
141
|
-
const sources = scenarios.map((scenario) => scenario.metrics?.cost_source).filter((source) => source !== undefined);
|
|
142
|
-
return sources.length === 0 ? null : sources.every((source) => source === sources[0]) ? sources[0] : "unreported";
|
|
143
|
-
})(),
|
|
144
|
-
tool_calls: sumOptional("tool_calls"),
|
|
145
|
-
delegated_children: sumOptional("delegated_children"),
|
|
146
|
-
max_concurrency: maxValues.length ? Math.max(...maxValues) : null,
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
function effective(result) {
|
|
150
|
-
if (result.override)
|
|
151
|
-
return result.override;
|
|
152
|
-
if (result.objective?.status === "ERROR")
|
|
153
|
-
return "ERROR";
|
|
154
|
-
if (result.objective?.status === "NOT-MEASURED")
|
|
155
|
-
return "NOT-MEASURED";
|
|
156
|
-
if (result.objective?.status === "FAIL")
|
|
157
|
-
return "FAIL";
|
|
158
|
-
return result.judge_verdict;
|
|
159
|
-
}
|
|
160
|
-
function untrustworthy(result, verdict) { return result.suspect || verdict === "ERROR" || verdict === "NOT-MEASURED" || verdict === "JUDGE-AMBIGUOUS"; }
|
|
161
|
-
function sorted(value) { return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b))); }
|
|
162
|
-
function increase(reference, candidate) { return reference === null || candidate === null || reference <= 0 ? null : (candidate - reference) / reference; }
|
|
163
|
-
function thresholdFindings(reference, candidate, thresholds) {
|
|
164
|
-
if (!thresholds)
|
|
165
|
-
return [];
|
|
166
|
-
const out = [];
|
|
167
|
-
const check = (label, left, right, max) => {
|
|
168
|
-
if (max === undefined)
|
|
169
|
-
return;
|
|
170
|
-
if (left === null || right === null) {
|
|
171
|
-
out.push(`${label} unavailable/incomplete — cannot evaluate configured ${(max * 100).toFixed(1)}% maximum`);
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
if (left === 0) {
|
|
175
|
-
if (right > 0)
|
|
176
|
-
out.push(`${label} increased from 0 to ${right}, exceeding the configured maximum`);
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
const delta = increase(left, right);
|
|
180
|
-
if (delta > max)
|
|
181
|
-
out.push(`${label} +${(delta * 100).toFixed(1)}% exceeds ${(max * 100).toFixed(1)}%`);
|
|
182
|
-
};
|
|
183
|
-
check("subject tokens", reference.input_tokens, candidate.input_tokens, thresholds.max_subject_token_increase);
|
|
184
|
-
check("wall time", reference.wall_time_ms, candidate.wall_time_ms, thresholds.max_wall_time_increase);
|
|
185
|
-
check("tool calls", reference.tool_calls, candidate.tool_calls, thresholds.max_tool_call_increase);
|
|
186
|
-
return out;
|
|
187
|
-
}
|
|
188
|
-
function metricDelta(reference, candidate) { return reference === null || candidate === null ? "unavailable" : `${reference} → ${candidate}`; }
|
|
189
|
-
//# sourceMappingURL=comparison.js.map
|