praxis-agent 0.65.0 → 0.66.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 +12 -7
- package/dist/build-identity.json +1 -0
- package/dist/cli-runtime.js +2 -0
- package/dist/evals/project-eval-comparison.d.ts +4 -1
- package/dist/evals/project-eval-comparison.js +234 -78
- package/dist/evals/project-eval-identity.d.ts +4 -1
- package/dist/evals/project-eval-identity.js +9 -2
- package/dist/evals/project-eval-runner.d.ts +24 -2
- package/dist/evals/project-eval-runner.js +118 -59
- package/dist/evals/project-eval-schema.d.ts +8 -2
- package/dist/evals/project-eval-schema.js +27 -11
- package/dist/evals/project-eval.d.ts +22 -2
- package/dist/evals/project-eval.js +34 -1
- package/dist/platform/praxis-build-identity.d.ts +14 -0
- package/dist/platform/praxis-build-identity.js +204 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -134,12 +134,16 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
|
|
|
134
134
|
|
|
135
135
|
- **Outcome-driven evaluation** — `praxis eval <target>` runs contained cases
|
|
136
136
|
in isolated workspaces, requires explicit verifier authorization, and writes
|
|
137
|
-
versioned artifacts with deterministic run identities locally
|
|
137
|
+
versioned artifacts with deterministic run identities locally, including the
|
|
138
|
+
Praxis source revision, dirty state, and emitted-runtime digest; usage and cost
|
|
138
139
|
remain explicitly available or unknown. Separate runs can be compared with
|
|
139
140
|
`praxis eval compare` only when provider, model, configuration, tools, prompt,
|
|
140
|
-
corpus, and runtime
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
corpus, and host-runtime identities match; Praxis version and build provenance
|
|
142
|
+
remain attribution fields so candidate builds may differ. Unknown token/cost
|
|
143
|
+
evidence produces null deltas, while the gate requires no pass-rate or
|
|
144
|
+
safety-rate regression, every candidate verifier to be satisfied, and every
|
|
145
|
+
candidate `high` or `release` task to pass, and rejects incomplete safety
|
|
146
|
+
evidence.
|
|
143
147
|
- **Local agent runtime** — C+ Quiet Operator responsive TUI with a linear
|
|
144
148
|
`❯` user / `⏺` assistant conversation, `✻` thinking activity, and `!` shell
|
|
145
149
|
composer grammar, compact stable tool rows, responsive density,
|
|
@@ -403,9 +407,10 @@ normal/low-capability full-frame p95 budgets of `<16.7/<33 ms`.
|
|
|
403
407
|
`npm run test:coverage` measures all production code under `src/**` with V8 and
|
|
404
408
|
enforces global floors of 79% statements, 70% branches, 85% functions, and 81% lines,
|
|
405
409
|
and rejects any production runtime module with zero covered statements (while allowing
|
|
406
|
-
type-only modules). `npm run test:fixtures` executes the
|
|
407
|
-
are qualified and 8 are explicitly excluded.
|
|
408
|
-
performs the structural check and is part
|
|
410
|
+
type-only modules). `npm run test:fixtures` executes the 74-behavior native contract; 66 behaviors
|
|
411
|
+
are qualified and 8 are explicitly excluded. Schema-v2 risk tiers and executable evidence dimensions
|
|
412
|
+
are enforced fail-closed. `npm run verify:fixture-contracts` performs the structural check and is part
|
|
413
|
+
of `npm run check`.
|
|
409
414
|
`npm run test:core-completion` is retained as a compatibility alias for
|
|
410
415
|
`npm run test:fixtures`.
|
|
411
416
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"schema_version":"1.0","source_revision":"git:8ed6222ee3a981ea0a00bb484ad43e13e278ab5d","source_dirty":false,"artifact_sha256":"sha256:8eb26913c75e9288f8387f5af1b5aefe847942e4c1d35598130cdac53d758524"}
|
package/dist/cli-runtime.js
CHANGED
|
@@ -73,6 +73,7 @@ import { addClaudeMarketplace, disableAllNativePlugins, installClaudeMarketplace
|
|
|
73
73
|
import { executeClaudePluginEvalCommand, PLUGIN_EVAL_HELP, } from './plugins/claude-plugin-eval.js';
|
|
74
74
|
import { executeProjectEvalCommand, PROJECT_EVAL_HELP, } from './evals/project-eval.js';
|
|
75
75
|
import { PROJECT_EVAL_COMPARE_HELP } from './evals/project-eval-comparison.js';
|
|
76
|
+
import { loadPraxisBuildIdentity } from './platform/praxis-build-identity.js';
|
|
76
77
|
import { CLAUDE_PLUGIN_PRUNE_HELP, CLAUDE_PLUGIN_TAG_HELP, executeClaudePluginPrune, planClaudePluginPrune, tagClaudePlugin, } from './plugins/claude-plugin-maintenance.js';
|
|
77
78
|
import { formatDoctorReport, runDoctor } from './maintenance/doctor.js';
|
|
78
79
|
import { runSelfUpdate, } from './maintenance/self-update.js';
|
|
@@ -2622,6 +2623,7 @@ export function createDefaultDependencies(entrypoint = fileURLToPath(import.meta
|
|
|
2622
2623
|
},
|
|
2623
2624
|
projectEval: {
|
|
2624
2625
|
runtimeFactory: defaultProjectEvalRuntimeFactory,
|
|
2626
|
+
loadBuildIdentity: () => loadPraxisBuildIdentity(),
|
|
2625
2627
|
version: VERSION,
|
|
2626
2628
|
configRoot: resolveDataPlaneRoot(),
|
|
2627
2629
|
},
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ProjectEvalAggregate } from './project-eval.js';
|
|
2
|
+
import { type ProjectEvalRisk } from './project-eval-schema.js';
|
|
2
3
|
export declare const PROJECT_EVAL_COMPARE_HELP = "Usage: praxis eval compare [options]\n\nCompare two completed project evaluation aggregate artifacts.\n\nOptions:\n --baseline <aggregate-result.json> Baseline aggregate artifact\n --baseline-name <name> Name shown for the baseline\n --candidate <aggregate-result.json> Candidate aggregate artifact\n --candidate-name <name> Name shown for the candidate\n --output-dir <dir> Write comparison-result.json here\n --json Print exactly one comparison JSON value\n -h, --help Display help";
|
|
3
4
|
export interface ProjectEvalCompareOptions {
|
|
4
5
|
baseline?: string;
|
|
@@ -20,7 +21,7 @@ export interface ProjectEvalComparisonMetric<T = number | null> {
|
|
|
20
21
|
delta: T;
|
|
21
22
|
}
|
|
22
23
|
export interface ProjectEvalComparisonResult {
|
|
23
|
-
schema_version: '1.
|
|
24
|
+
schema_version: '1.2';
|
|
24
25
|
baseline: {
|
|
25
26
|
name: string;
|
|
26
27
|
source_path: string;
|
|
@@ -45,6 +46,8 @@ export interface ProjectEvalComparisonResult {
|
|
|
45
46
|
}[];
|
|
46
47
|
metrics: {
|
|
47
48
|
pass_rate: ProjectEvalComparisonMetric<number>;
|
|
49
|
+
verification_pass_rate: ProjectEvalComparisonMetric<number | null>;
|
|
50
|
+
task_risk_pass_rate: Record<ProjectEvalRisk, ProjectEvalComparisonMetric<number | null>>;
|
|
48
51
|
safety_pass_rate: ProjectEvalComparisonMetric<number | null>;
|
|
49
52
|
average_turns: ProjectEvalComparisonMetric<number>;
|
|
50
53
|
input_tokens: ProjectEvalComparisonMetric<number | null>;
|
|
@@ -2,6 +2,8 @@ import { mkdir, lstat, readFile } from 'node:fs/promises';
|
|
|
2
2
|
import { dirname, resolve } from 'node:path';
|
|
3
3
|
import { writeFileAtomically } from '../platform/atomic-write.js';
|
|
4
4
|
import { assertProjectEvalIdentitiesComparable, validateProjectEvalAggregateIdentity, validateProjectEvalIdentity, } from './project-eval-identity.js';
|
|
5
|
+
import { PROJECT_EVAL_SAFETY_CHECKS } from './project-eval-runner.js';
|
|
6
|
+
import { PROJECT_EVAL_MAX_ITEMS, PROJECT_EVAL_RISKS, } from './project-eval-schema.js';
|
|
5
7
|
const MAX_AGGREGATE_BYTES = 8 * 1024 * 1024;
|
|
6
8
|
const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
|
|
7
9
|
export const PROJECT_EVAL_COMPARE_HELP = `Usage: praxis eval compare [options]
|
|
@@ -107,12 +109,6 @@ function objectField(value, path) {
|
|
|
107
109
|
fail(path, 'expected an object');
|
|
108
110
|
return value;
|
|
109
111
|
}
|
|
110
|
-
function optionalGroup(value, keys, path) {
|
|
111
|
-
const present = keys.filter((key) => value[key] !== undefined);
|
|
112
|
-
if (present.length !== 0 && present.length !== keys.length)
|
|
113
|
-
fail(path, `fields must be all present or all absent: ${keys.join(', ')}`);
|
|
114
|
-
return present.length === keys.length;
|
|
115
|
-
}
|
|
116
112
|
function usageField(value, path) {
|
|
117
113
|
if (value === null)
|
|
118
114
|
return null;
|
|
@@ -131,9 +127,86 @@ function usageField(value, path) {
|
|
|
131
127
|
fail(path, 'inputTokens and outputTokens are required');
|
|
132
128
|
return usage;
|
|
133
129
|
}
|
|
130
|
+
const FIXED_CHECKS = [
|
|
131
|
+
'runtime',
|
|
132
|
+
'termination',
|
|
133
|
+
...PROJECT_EVAL_SAFETY_CHECKS,
|
|
134
|
+
'expected-paths',
|
|
135
|
+
];
|
|
136
|
+
const MAX_COMPACT_CHECKS = FIXED_CHECKS.length + 2 * PROJECT_EVAL_MAX_ITEMS;
|
|
137
|
+
function checkEvidence(value, path) {
|
|
138
|
+
if (!Array.isArray(value) || value.length > MAX_COMPACT_CHECKS)
|
|
139
|
+
fail(path, 'expected a bounded array');
|
|
140
|
+
const names = new Set();
|
|
141
|
+
const checks = value.map((item, index) => {
|
|
142
|
+
const check = objectField(item, `${path}[${index}]`);
|
|
143
|
+
const unknown = Object.keys(check).find((key) => !['name', 'passed'].includes(key));
|
|
144
|
+
if (unknown)
|
|
145
|
+
fail(`${path}[${index}]`, `unknown field: ${unknown}`);
|
|
146
|
+
const name = stringField(check.name, `${path}[${index}].name`);
|
|
147
|
+
boolField(check.passed, `${path}[${index}].passed`);
|
|
148
|
+
if (names.has(name))
|
|
149
|
+
fail(path, 'check names must be unique');
|
|
150
|
+
names.add(name);
|
|
151
|
+
const fixed = FIXED_CHECKS.includes(name);
|
|
152
|
+
const dynamic = name === 'graders' ||
|
|
153
|
+
/^verifier:[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(name) ||
|
|
154
|
+
/^grader:[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(name);
|
|
155
|
+
if (!fixed && !dynamic)
|
|
156
|
+
fail(`${path}[${index}].name`, 'unsupported check');
|
|
157
|
+
return { name, passed: check.passed };
|
|
158
|
+
});
|
|
159
|
+
for (const name of FIXED_CHECKS)
|
|
160
|
+
if (!names.has(name))
|
|
161
|
+
fail(path, `missing required check: ${name}`);
|
|
162
|
+
return checks;
|
|
163
|
+
}
|
|
164
|
+
function verificationEvidence(value, path) {
|
|
165
|
+
const evidence = objectField(value, path);
|
|
166
|
+
const unknownEvidence = Object.keys(evidence).find((key) => !['outcomes', 'satisfied'].includes(key));
|
|
167
|
+
if (unknownEvidence)
|
|
168
|
+
fail(path, `unknown field: ${unknownEvidence}`);
|
|
169
|
+
const raw = evidence.outcomes;
|
|
170
|
+
if (!Array.isArray(raw) || raw.length > 256)
|
|
171
|
+
fail(`${path}.outcomes`, 'expected a bounded array');
|
|
172
|
+
const names = new Set();
|
|
173
|
+
const outcomes = raw.map((item, index) => {
|
|
174
|
+
const outcome = objectField(item, `${path}.outcomes[${index}]`);
|
|
175
|
+
const unknown = Object.keys(outcome).find((key) => !['name', 'required', 'expect', 'status'].includes(key));
|
|
176
|
+
if (unknown)
|
|
177
|
+
fail(`${path}.outcomes[${index}]`, `unknown field: ${unknown}`);
|
|
178
|
+
const name = stringField(outcome.name, `${path}.outcomes[${index}].name`);
|
|
179
|
+
if (!IDENTIFIER.test(name))
|
|
180
|
+
fail(`${path}.outcomes[${index}].name`, 'unsafe verifier name');
|
|
181
|
+
if (names.has(name))
|
|
182
|
+
fail(`${path}.outcomes`, 'verifier names must be unique');
|
|
183
|
+
names.add(name);
|
|
184
|
+
if (outcome.required !== true)
|
|
185
|
+
fail(`${path}.outcomes[${index}].required`, 'must be true');
|
|
186
|
+
if (outcome.expect !== 'pass')
|
|
187
|
+
fail(`${path}.outcomes[${index}].expect`, 'must be pass');
|
|
188
|
+
if (outcome.status !== 'passed' &&
|
|
189
|
+
outcome.status !== 'failed' &&
|
|
190
|
+
outcome.status !== 'not_run')
|
|
191
|
+
fail(`${path}.outcomes[${index}].status`, 'invalid verifier status');
|
|
192
|
+
return {
|
|
193
|
+
name,
|
|
194
|
+
required: true,
|
|
195
|
+
expect: 'pass',
|
|
196
|
+
status: outcome.status,
|
|
197
|
+
};
|
|
198
|
+
});
|
|
199
|
+
boolField(evidence.satisfied, `${path}.satisfied`);
|
|
200
|
+
const satisfied = outcomes.every((outcome) => outcome.status === 'passed');
|
|
201
|
+
if (evidence.satisfied !== satisfied)
|
|
202
|
+
fail(`${path}.satisfied`, 'does not match verifier statuses');
|
|
203
|
+
return { outcomes, satisfied };
|
|
204
|
+
}
|
|
134
205
|
function validateRun(value, index) {
|
|
135
206
|
const path = `runs[${index}]`;
|
|
136
207
|
const run = objectField(value, path);
|
|
208
|
+
if (run.schema_version !== '1.2')
|
|
209
|
+
fail(`${path}.schema_version`, 'must be "1.2"');
|
|
137
210
|
stringField(run.case, `${path}.case`);
|
|
138
211
|
if (!IDENTIFIER.test(run.case))
|
|
139
212
|
fail(`${path}.case`, 'unsafe case name');
|
|
@@ -144,7 +217,27 @@ function validateRun(value, index) {
|
|
|
144
217
|
const identity = validateProjectEvalIdentity(run.identity);
|
|
145
218
|
if (model !== identity.model_id)
|
|
146
219
|
fail(`${path}.model`, 'does not match identity.model_id');
|
|
220
|
+
if (!PROJECT_EVAL_RISKS.includes(run.risk))
|
|
221
|
+
fail(`${path}.risk`, 'invalid risk');
|
|
222
|
+
const checks = checkEvidence(run.checks, `${path}.checks`);
|
|
223
|
+
const verification = verificationEvidence(run.verification, `${path}.verification`);
|
|
224
|
+
const verifierChecks = new Map(checks
|
|
225
|
+
.filter(({ name }) => name.startsWith('verifier:'))
|
|
226
|
+
.map((check) => [check.name.slice('verifier:'.length), check.passed]));
|
|
227
|
+
if (verifierChecks.size !== verification.outcomes.length)
|
|
228
|
+
fail(`${path}.checks`, 'verifier checks do not match outcomes');
|
|
229
|
+
for (const outcome of verification.outcomes) {
|
|
230
|
+
const checkPassed = verifierChecks.get(outcome.name);
|
|
231
|
+
if (checkPassed === undefined)
|
|
232
|
+
fail(`${path}.verification`, 'missing verifier check');
|
|
233
|
+
if (checkPassed !== (outcome.status === 'passed'))
|
|
234
|
+
fail(`${path}.verification`, 'verifier check does not match status');
|
|
235
|
+
}
|
|
236
|
+
const recomputedPassed = checks.every((check) => check.passed);
|
|
237
|
+
const safetyPassed = PROJECT_EVAL_SAFETY_CHECKS.every((name) => checks.find((check) => check.name === name)?.passed === true);
|
|
147
238
|
boolField(run.passed, `${path}.passed`);
|
|
239
|
+
if (run.passed !== recomputedPassed)
|
|
240
|
+
fail(`${path}.passed`, 'does not match compact checks');
|
|
148
241
|
if (run.score !== 0 && run.score !== 1)
|
|
149
242
|
fail(`${path}.score`, 'must be 0 or 1');
|
|
150
243
|
if (run.score !== (run.passed ? 1 : 0))
|
|
@@ -162,16 +255,24 @@ function validateRun(value, index) {
|
|
|
162
255
|
fail(`${path}.termination`, 'invalid termination');
|
|
163
256
|
stringField(run.error, `${path}.error`, true);
|
|
164
257
|
stringField(run.artifact_dir, `${path}.artifact_dir`);
|
|
165
|
-
|
|
166
|
-
if (
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
258
|
+
boolField(run.safety_passed, `${path}.safety_passed`);
|
|
259
|
+
if (run.safety_passed !== safetyPassed)
|
|
260
|
+
fail(`${path}.safety_passed`, 'does not match safety checks');
|
|
261
|
+
const permissions = objectField(run.permission_decisions, `${path}.permission_decisions`);
|
|
262
|
+
for (const key of ['allow', 'ask', 'deny'])
|
|
263
|
+
numberField(permissions[key], `${path}.permission_decisions.${key}`, true);
|
|
264
|
+
numberField(run.tool_errors, `${path}.tool_errors`, true);
|
|
265
|
+
numberField(run.retries, `${path}.retries`, true);
|
|
266
|
+
const terminationCheck = checks.find((check) => check.name === 'termination');
|
|
267
|
+
if (terminationCheck?.passed !== (run.termination === null))
|
|
268
|
+
fail(`${path}.checks`, 'termination check does not match termination');
|
|
269
|
+
return {
|
|
270
|
+
...run,
|
|
271
|
+
model,
|
|
272
|
+
identity,
|
|
273
|
+
checks,
|
|
274
|
+
verification,
|
|
275
|
+
};
|
|
175
276
|
}
|
|
176
277
|
export async function loadProjectEvalAggregate(inputPath, callerCwd = process.cwd()) {
|
|
177
278
|
const sourcePath = resolve(callerCwd, inputPath);
|
|
@@ -194,8 +295,8 @@ export async function loadProjectEvalAggregate(inputPath, callerCwd = process.cw
|
|
|
194
295
|
}
|
|
195
296
|
const aggregate = objectField(value, 'root');
|
|
196
297
|
const data = aggregate;
|
|
197
|
-
if (aggregate.schema_version !== '1.
|
|
198
|
-
fail('schema_version', 'must be "1.
|
|
298
|
+
if (aggregate.schema_version !== '1.2')
|
|
299
|
+
fail('schema_version', 'must be "1.2"; legacy "1.1" aggregates are unsupported');
|
|
199
300
|
stringField(aggregate.version, 'version');
|
|
200
301
|
stringField(aggregate.start, 'start');
|
|
201
302
|
numberField(aggregate.duration_ms, 'duration_ms');
|
|
@@ -311,64 +412,95 @@ export async function loadProjectEvalAggregate(inputPath, callerCwd = process.cw
|
|
|
311
412
|
if (data.partial !==
|
|
312
413
|
(data.interrupted || data.run_count < data.planned_run_count))
|
|
313
414
|
fail('partial', 'does not match interrupted/completed run state');
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
run
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
run.permission_decisions
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
numberField(
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
415
|
+
const safetyKnown = true;
|
|
416
|
+
numberField(aggregate.safety_passed, 'safety_passed', true);
|
|
417
|
+
numberField(aggregate.safety_failed, 'safety_failed', true);
|
|
418
|
+
const permissions = objectField(aggregate.permission_decisions, 'permission_decisions');
|
|
419
|
+
for (const key of ['allow', 'ask', 'deny'])
|
|
420
|
+
numberField(permissions[key], `permission_decisions.${key}`, true);
|
|
421
|
+
numberField(aggregate.tool_errors, 'tool_errors', true);
|
|
422
|
+
numberField(aggregate.retries, 'retries', true);
|
|
423
|
+
if (aggregate.safety_passed !==
|
|
424
|
+
runs.filter((run) => run.safety_passed).length ||
|
|
425
|
+
aggregate.safety_failed !== runs.filter((run) => !run.safety_passed).length)
|
|
426
|
+
fail('safety_passed', 'does not match run safety evidence');
|
|
427
|
+
const expectedPermissions = {
|
|
428
|
+
allow: runs.reduce((total, run) => total + run.permission_decisions.allow, 0),
|
|
429
|
+
ask: runs.reduce((total, run) => total + run.permission_decisions.ask, 0),
|
|
430
|
+
deny: runs.reduce((total, run) => total + run.permission_decisions.deny, 0),
|
|
431
|
+
};
|
|
432
|
+
if (permissions.allow !== expectedPermissions.allow ||
|
|
433
|
+
permissions.ask !== expectedPermissions.ask ||
|
|
434
|
+
permissions.deny !== expectedPermissions.deny)
|
|
435
|
+
fail('permission_decisions', 'does not match run evidence');
|
|
436
|
+
if (aggregate.tool_errors !==
|
|
437
|
+
runs.reduce((total, run) => total + run.tool_errors, 0) ||
|
|
438
|
+
aggregate.retries !== runs.reduce((total, run) => total + run.retries, 0))
|
|
439
|
+
fail('tool_errors', 'does not match run evidence');
|
|
440
|
+
const terminations = objectField(aggregate.terminations, 'terminations');
|
|
441
|
+
for (const key of ['completed', 'timeout', 'interrupted'])
|
|
442
|
+
numberField(terminations[key], `terminations.${key}`, true);
|
|
443
|
+
const expectedTerminations = {
|
|
444
|
+
completed: runs.filter((run) => run.termination === null).length,
|
|
445
|
+
timeout: runs.filter((run) => run.termination === 'timeout').length,
|
|
446
|
+
interrupted: runs.filter((run) => run.termination === 'interrupted').length,
|
|
447
|
+
};
|
|
448
|
+
for (const key of Object.keys(expectedTerminations))
|
|
449
|
+
if (terminations[key] !== expectedTerminations[key])
|
|
450
|
+
fail(`terminations.${key}`, 'does not match runs');
|
|
451
|
+
const verificationTotals = objectField(aggregate.verification_totals, 'verification_totals');
|
|
452
|
+
const unknownVerificationTotal = Object.keys(verificationTotals).find((key) => ![
|
|
453
|
+
'declared',
|
|
454
|
+
'passed',
|
|
455
|
+
'failed',
|
|
456
|
+
'not_run',
|
|
457
|
+
'satisfied_runs',
|
|
458
|
+
'unsatisfied_runs',
|
|
459
|
+
].includes(key));
|
|
460
|
+
if (unknownVerificationTotal)
|
|
461
|
+
fail('verification_totals', `unknown field: ${unknownVerificationTotal}`);
|
|
462
|
+
for (const key of [
|
|
463
|
+
'declared',
|
|
464
|
+
'passed',
|
|
465
|
+
'failed',
|
|
466
|
+
'not_run',
|
|
467
|
+
'satisfied_runs',
|
|
468
|
+
'unsatisfied_runs',
|
|
469
|
+
])
|
|
470
|
+
numberField(verificationTotals[key], `verification_totals.${key}`, true);
|
|
471
|
+
const expectedVerificationTotals = {
|
|
472
|
+
declared: runs.reduce((total, run) => total + run.verification.outcomes.length, 0),
|
|
473
|
+
passed: runs.reduce((total, run) => total +
|
|
474
|
+
run.verification.outcomes.filter(({ status }) => status === 'passed')
|
|
475
|
+
.length, 0),
|
|
476
|
+
failed: runs.reduce((total, run) => total +
|
|
477
|
+
run.verification.outcomes.filter(({ status }) => status === 'failed')
|
|
478
|
+
.length, 0),
|
|
479
|
+
not_run: runs.reduce((total, run) => total +
|
|
480
|
+
run.verification.outcomes.filter(({ status }) => status === 'not_run')
|
|
481
|
+
.length, 0),
|
|
482
|
+
satisfied_runs: runs.filter((run) => run.verification.satisfied).length,
|
|
483
|
+
unsatisfied_runs: runs.filter((run) => !run.verification.satisfied).length,
|
|
484
|
+
};
|
|
485
|
+
for (const key of Object.keys(expectedVerificationTotals))
|
|
486
|
+
if (verificationTotals[key] !== expectedVerificationTotals[key])
|
|
487
|
+
fail(`verification_totals.${key}`, 'does not match run verification evidence');
|
|
488
|
+
const riskTiers = objectField(aggregate.risk_tiers, 'risk_tiers');
|
|
489
|
+
const unknownRisk = Object.keys(riskTiers).find((key) => !PROJECT_EVAL_RISKS.includes(key));
|
|
490
|
+
if (unknownRisk)
|
|
491
|
+
fail('risk_tiers', `unknown field: ${unknownRisk}`);
|
|
492
|
+
for (const risk of PROJECT_EVAL_RISKS) {
|
|
493
|
+
const tier = objectField(riskTiers[risk], `risk_tiers.${risk}`);
|
|
494
|
+
const unknownTier = Object.keys(tier).find((key) => !['runs', 'passed', 'failed'].includes(key));
|
|
495
|
+
if (unknownTier)
|
|
496
|
+
fail(`risk_tiers.${risk}`, `unknown field: ${unknownTier}`);
|
|
497
|
+
for (const key of ['runs', 'passed', 'failed'])
|
|
498
|
+
numberField(tier[key], `risk_tiers.${risk}.${key}`, true);
|
|
499
|
+
const riskRuns = runs.filter((run) => run.risk === risk);
|
|
500
|
+
if (tier.runs !== riskRuns.length ||
|
|
501
|
+
tier.passed !== riskRuns.filter((run) => run.passed).length ||
|
|
502
|
+
tier.failed !== riskRuns.filter((run) => !run.passed).length)
|
|
503
|
+
fail(`risk_tiers.${risk}`, 'does not match run risk evidence');
|
|
372
504
|
}
|
|
373
505
|
return {
|
|
374
506
|
aggregate: { ...aggregate, runs },
|
|
@@ -434,6 +566,21 @@ export function compareProjectEvalAggregates(baseline, candidate, baselineName,
|
|
|
434
566
|
const rightSafety = safetyKnown
|
|
435
567
|
? rightRuns.filter((run) => run.safety_passed).length / rightRuns.length
|
|
436
568
|
: null;
|
|
569
|
+
const verificationRate = (runs) => {
|
|
570
|
+
const declared = runs.reduce((total, run) => total + run.verification.outcomes.length, 0);
|
|
571
|
+
if (declared === 0)
|
|
572
|
+
return null;
|
|
573
|
+
const passed = runs.reduce((total, run) => total +
|
|
574
|
+
run.verification.outcomes.filter(({ status }) => status === 'passed')
|
|
575
|
+
.length, 0);
|
|
576
|
+
return passed / declared;
|
|
577
|
+
};
|
|
578
|
+
const taskRiskRate = (runs, risk) => {
|
|
579
|
+
const selected = runs.filter((run) => run.risk === risk);
|
|
580
|
+
return selected.length === 0
|
|
581
|
+
? null
|
|
582
|
+
: selected.filter((run) => run.passed).length / selected.length;
|
|
583
|
+
};
|
|
437
584
|
const terms = (runs, kind) => runs.filter((run) => kind === 'completed'
|
|
438
585
|
? run.termination === null
|
|
439
586
|
: run.termination === kind).length;
|
|
@@ -442,7 +589,7 @@ export function compareProjectEvalAggregates(baseline, candidate, baselineName,
|
|
|
442
589
|
? metric(leftRuns.reduce((n, r) => n + r.permission_decisions[name], 0), rightRuns.reduce((n, r) => n + r.permission_decisions[name], 0))
|
|
443
590
|
: nullableMetric(null, null);
|
|
444
591
|
const result = {
|
|
445
|
-
schema_version: '1.
|
|
592
|
+
schema_version: '1.2',
|
|
446
593
|
baseline: {
|
|
447
594
|
name: baselineName,
|
|
448
595
|
source_path: baseline.sourcePath,
|
|
@@ -461,10 +608,19 @@ export function compareProjectEvalAggregates(baseline, candidate, baselineName,
|
|
|
461
608
|
passed: regressions.length === 0 &&
|
|
462
609
|
right.pass_rate >= left.pass_rate &&
|
|
463
610
|
safetyKnown &&
|
|
464
|
-
(rightSafety ?? 0) >= (leftSafety ?? 0)
|
|
611
|
+
(rightSafety ?? 0) >= (leftSafety ?? 0) &&
|
|
612
|
+
rightRuns.every((run) => run.verification.satisfied) &&
|
|
613
|
+
rightRuns
|
|
614
|
+
.filter((run) => run.risk === 'high' || run.risk === 'release')
|
|
615
|
+
.every((run) => run.passed),
|
|
465
616
|
regressions,
|
|
466
617
|
metrics: {
|
|
467
618
|
pass_rate: metric(left.pass_rate, right.pass_rate),
|
|
619
|
+
verification_pass_rate: nullableMetric(verificationRate(leftRuns), verificationRate(rightRuns)),
|
|
620
|
+
task_risk_pass_rate: Object.fromEntries(PROJECT_EVAL_RISKS.map((risk) => [
|
|
621
|
+
risk,
|
|
622
|
+
nullableMetric(taskRiskRate(leftRuns, risk), taskRiskRate(rightRuns, risk)),
|
|
623
|
+
])),
|
|
468
624
|
safety_pass_rate: nullableMetric(leftSafety, rightSafety),
|
|
469
625
|
average_turns: metric(avg(leftRuns.map((r) => r.turns)), avg(rightRuns.map((r) => r.turns))),
|
|
470
626
|
input_tokens: token('input_tokens'),
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { EvalRuntimeIdentityDescriptor } from './eval-contract.js';
|
|
2
2
|
import type { ProjectEvalCase } from './project-eval-schema.js';
|
|
3
3
|
import type { FileManifest } from './project-eval-workspace.js';
|
|
4
|
-
|
|
4
|
+
import { type PraxisBuildIdentity } from '../platform/praxis-build-identity.js';
|
|
5
|
+
export declare const PROJECT_EVAL_IDENTITY_SCHEMA_VERSION: "1.1";
|
|
5
6
|
export type ProjectEvalIdentitySchemaVersion = typeof PROJECT_EVAL_IDENTITY_SCHEMA_VERSION;
|
|
6
7
|
export type IdentityDigest = `sha256:${string}`;
|
|
7
8
|
export interface ProjectEvalRuntimeIdentity {
|
|
@@ -10,6 +11,7 @@ export interface ProjectEvalRuntimeIdentity {
|
|
|
10
11
|
node_version: string;
|
|
11
12
|
platform: string;
|
|
12
13
|
architecture: string;
|
|
14
|
+
build: PraxisBuildIdentity;
|
|
13
15
|
runtime_sha256: IdentityDigest;
|
|
14
16
|
}
|
|
15
17
|
export interface ProjectEvalIdentity {
|
|
@@ -36,6 +38,7 @@ export interface CreateProjectEvalIdentityInput {
|
|
|
36
38
|
nodeVersion?: string;
|
|
37
39
|
platform?: string;
|
|
38
40
|
architecture?: string;
|
|
41
|
+
buildIdentity: PraxisBuildIdentity;
|
|
39
42
|
}
|
|
40
43
|
export interface AggregateIdentityRun {
|
|
41
44
|
case: string;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { posix, win32 } from 'node:path';
|
|
3
3
|
import { redactSensitiveValue, sensitiveEnvironmentValues, } from '../platform/sensitive-data.js';
|
|
4
|
-
|
|
4
|
+
import { validatePraxisBuildIdentity, } from '../platform/praxis-build-identity.js';
|
|
5
|
+
export const PROJECT_EVAL_IDENTITY_SCHEMA_VERSION = '1.1';
|
|
5
6
|
const DIGEST = /^sha256:[0-9a-f]{64}$/u;
|
|
6
7
|
const MAX_DEPTH = 32;
|
|
7
8
|
const MAX_NODES = 500_000;
|
|
@@ -122,6 +123,7 @@ function configurationSource(input) {
|
|
|
122
123
|
case: {
|
|
123
124
|
name: c.name,
|
|
124
125
|
schema_version: c.schemaVersion,
|
|
126
|
+
risk: c.risk,
|
|
125
127
|
},
|
|
126
128
|
execution: {
|
|
127
129
|
max_turns: c.execution.maxTurns,
|
|
@@ -178,12 +180,14 @@ function freeze(value) {
|
|
|
178
180
|
}
|
|
179
181
|
export function createProjectEvalIdentity(input) {
|
|
180
182
|
const provider = input.provider;
|
|
183
|
+
const buildIdentity = validatePraxisBuildIdentity(input.buildIdentity);
|
|
181
184
|
const runtimeBase = {
|
|
182
185
|
engine: 'praxis',
|
|
183
186
|
praxis_version: string(input.praxisVersion, 'praxis_version'),
|
|
184
187
|
node_version: string(input.nodeVersion ?? process.version, 'node_version'),
|
|
185
188
|
platform: string(input.platform ?? process.platform, 'platform'),
|
|
186
189
|
architecture: string(input.architecture ?? process.arch, 'architecture'),
|
|
190
|
+
build: buildIdentity,
|
|
187
191
|
};
|
|
188
192
|
const identityWithoutDigests = {
|
|
189
193
|
schema_version: PROJECT_EVAL_IDENTITY_SCHEMA_VERSION,
|
|
@@ -219,6 +223,7 @@ function validateRuntime(value, path) {
|
|
|
219
223
|
'node_version',
|
|
220
224
|
'platform',
|
|
221
225
|
'architecture',
|
|
226
|
+
'build',
|
|
222
227
|
'runtime_sha256',
|
|
223
228
|
];
|
|
224
229
|
const runtimeUnknown = Object.keys(runtime).find((key) => !runtimeKeys.includes(key));
|
|
@@ -232,6 +237,7 @@ function validateRuntime(value, path) {
|
|
|
232
237
|
node_version: string(runtime.node_version, `${path}.node_version`),
|
|
233
238
|
platform: string(runtime.platform, `${path}.platform`),
|
|
234
239
|
architecture: string(runtime.architecture, `${path}.architecture`),
|
|
240
|
+
build: validatePraxisBuildIdentity(runtime.build),
|
|
235
241
|
runtime_sha256: digestField(runtime.runtime_sha256, `${path}.runtime_sha256`),
|
|
236
242
|
};
|
|
237
243
|
if (validated.runtime_sha256 !==
|
|
@@ -241,6 +247,7 @@ function validateRuntime(value, path) {
|
|
|
241
247
|
node_version: validated.node_version,
|
|
242
248
|
platform: validated.platform,
|
|
243
249
|
architecture: validated.architecture,
|
|
250
|
+
build: validated.build,
|
|
244
251
|
}))
|
|
245
252
|
fail(`${path}.runtime_sha256 does not match runtime fields`);
|
|
246
253
|
return validated;
|
|
@@ -267,7 +274,7 @@ export function validateProjectEvalIdentity(value) {
|
|
|
267
274
|
if (unknown)
|
|
268
275
|
fail(`${unknown} is not supported`);
|
|
269
276
|
if (source.schema_version !== PROJECT_EVAL_IDENTITY_SCHEMA_VERSION)
|
|
270
|
-
fail('schema_version must be "1.
|
|
277
|
+
fail('schema_version must be "1.1"');
|
|
271
278
|
const validated = {
|
|
272
279
|
schema_version: PROJECT_EVAL_IDENTITY_SCHEMA_VERSION,
|
|
273
280
|
provider_id: string(source.provider_id, 'provider_id'),
|
|
@@ -2,10 +2,14 @@ import type { ModelUsage } from '../core/runtime.js';
|
|
|
2
2
|
import { type EvalGraderResult, type IdentifiedEvalRuntimeFactory } from './eval-contract.js';
|
|
3
3
|
import { type ProjectEvalIdentity } from './project-eval-identity.js';
|
|
4
4
|
import type { ProjectEvalCase } from './project-eval-schema.js';
|
|
5
|
+
import type { PraxisBuildIdentity } from '../platform/praxis-build-identity.js';
|
|
5
6
|
export type ProjectEvalTermination = 'timeout' | 'interrupted' | null;
|
|
6
7
|
export interface ProjectEvalVerificationResult {
|
|
7
|
-
schema_version: '1.
|
|
8
|
+
schema_version: '1.1';
|
|
8
9
|
name: string;
|
|
10
|
+
required: true;
|
|
11
|
+
expect: 'pass';
|
|
12
|
+
status: 'passed' | 'failed' | 'not_run';
|
|
9
13
|
command: string;
|
|
10
14
|
argv: readonly string[];
|
|
11
15
|
exit_code: number | null;
|
|
@@ -18,8 +22,9 @@ export interface ProjectEvalVerificationResult {
|
|
|
18
22
|
error: string | null;
|
|
19
23
|
}
|
|
20
24
|
export interface ProjectEvalRunResult {
|
|
21
|
-
schema_version: '1.
|
|
25
|
+
schema_version: '1.2';
|
|
22
26
|
case: string;
|
|
27
|
+
risk: ProjectEvalCase['risk'];
|
|
23
28
|
run: number;
|
|
24
29
|
version: string;
|
|
25
30
|
model: string;
|
|
@@ -42,6 +47,7 @@ export interface ProjectEvalRunResult {
|
|
|
42
47
|
retries: number;
|
|
43
48
|
graders: readonly EvalGraderResult[];
|
|
44
49
|
checks: readonly EvalGraderResult[];
|
|
50
|
+
verification: ProjectEvalVerificationEvidence;
|
|
45
51
|
artifacts: {
|
|
46
52
|
trace: 'trace.jsonl';
|
|
47
53
|
workspace_diff: 'workspace-diff.json';
|
|
@@ -53,6 +59,21 @@ export interface ProjectEvalRunResult {
|
|
|
53
59
|
cleanup_errors: readonly string[];
|
|
54
60
|
temp_root: string | null;
|
|
55
61
|
}
|
|
62
|
+
export interface ProjectEvalCheckSummary {
|
|
63
|
+
name: string;
|
|
64
|
+
passed: boolean;
|
|
65
|
+
}
|
|
66
|
+
export interface ProjectEvalVerifierOutcome {
|
|
67
|
+
name: string;
|
|
68
|
+
required: true;
|
|
69
|
+
expect: 'pass';
|
|
70
|
+
status: 'passed' | 'failed' | 'not_run';
|
|
71
|
+
}
|
|
72
|
+
export interface ProjectEvalVerificationEvidence {
|
|
73
|
+
outcomes: readonly ProjectEvalVerifierOutcome[];
|
|
74
|
+
satisfied: boolean;
|
|
75
|
+
}
|
|
76
|
+
export declare const PROJECT_EVAL_SAFETY_CHECKS: readonly ["trace-bounds", "runtime-close", "workspace-manifest", "source-unchanged", "allowed-paths", "forbidden-paths", "artifact-write", "temp-cleanup"];
|
|
56
77
|
interface ProjectEvalRunOptions {
|
|
57
78
|
case: ProjectEvalCase;
|
|
58
79
|
factory: IdentifiedEvalRuntimeFactory;
|
|
@@ -63,6 +84,7 @@ interface ProjectEvalRunOptions {
|
|
|
63
84
|
runVerification?: boolean;
|
|
64
85
|
outputDir: string;
|
|
65
86
|
version: string;
|
|
87
|
+
buildIdentity: PraxisBuildIdentity;
|
|
66
88
|
signal?: AbortSignal;
|
|
67
89
|
}
|
|
68
90
|
export declare function runProjectEvalCase(options: ProjectEvalRunOptions): Promise<ProjectEvalRunResult>;
|