praxis-agent 0.64.1 → 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 +14 -7
- package/dist/build-identity.json +1 -0
- package/dist/cli-runtime.js +25 -2
- package/dist/evals/eval-contract.d.ts +11 -0
- package/dist/evals/project-eval-comparison.d.ts +6 -1
- package/dist/evals/project-eval-comparison.js +261 -79
- package/dist/evals/project-eval-identity.d.ts +54 -0
- package/dist/evals/project-eval-identity.js +348 -0
- package/dist/evals/project-eval-runner.d.ts +30 -5
- package/dist/evals/project-eval-runner.js +156 -75
- 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 +28 -5
- package/dist/evals/project-eval.js +47 -3
- package/dist/platform/praxis-build-identity.d.ts +14 -0
- package/dist/platform/praxis-build-identity.js +204 -0
- package/dist/providers/provider-registry.d.ts +2 -1
- package/dist/providers/provider-registry.js +23 -9
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -134,10 +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
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
137
|
+
versioned artifacts with deterministic run identities locally, including the
|
|
138
|
+
Praxis source revision, dirty state, and emitted-runtime digest; usage and cost
|
|
139
|
+
remain explicitly available or unknown. Separate runs can be compared with
|
|
140
|
+
`praxis eval compare` only when provider, model, configuration, tools, prompt,
|
|
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.
|
|
141
147
|
- **Local agent runtime** — C+ Quiet Operator responsive TUI with a linear
|
|
142
148
|
`❯` user / `⏺` assistant conversation, `✻` thinking activity, and `!` shell
|
|
143
149
|
composer grammar, compact stable tool rows, responsive density,
|
|
@@ -401,9 +407,10 @@ normal/low-capability full-frame p95 budgets of `<16.7/<33 ms`.
|
|
|
401
407
|
`npm run test:coverage` measures all production code under `src/**` with V8 and
|
|
402
408
|
enforces global floors of 79% statements, 70% branches, 85% functions, and 81% lines,
|
|
403
409
|
and rejects any production runtime module with zero covered statements (while allowing
|
|
404
|
-
type-only modules). `npm run test:fixtures` executes the
|
|
405
|
-
are qualified and 8 are explicitly excluded.
|
|
406
|
-
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`.
|
|
407
414
|
`npm run test:core-completion` is retained as a compatibility alias for
|
|
408
415
|
`npm run test:fixtures`.
|
|
409
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
|
@@ -51,7 +51,7 @@ import { FallbackModelProvider } from './providers/fallback-provider.js';
|
|
|
51
51
|
import { ProviderCredentialVault } from './persistence/provider-credential-vault.js';
|
|
52
52
|
import { parseContextEnvironment, parseProviderEnvironment, } from './providers/environment.js';
|
|
53
53
|
import { ProviderAuthenticationError, resolveProviderCredential, } from './providers/provider-auth.js';
|
|
54
|
-
import { resolveProviderContextWindowTokens, resolveProviderRegistry, } from './providers/provider-registry.js';
|
|
54
|
+
import { resolveProviderContextWindowTokens, resolveProviderRegistry, resolveProviderRuntimeTarget, } from './providers/provider-registry.js';
|
|
55
55
|
import { ProviderSettingsError, resolveProviderTarget, } from './providers/provider-settings.js';
|
|
56
56
|
import { ModelPricingRegistry, usageCostUsd } from './core/usage.js';
|
|
57
57
|
import { LocalToolRegistry } from './tools/local-tools.js';
|
|
@@ -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';
|
|
@@ -2358,6 +2359,27 @@ const defaultPluginEvalRuntimeFactory = {
|
|
|
2358
2359
|
};
|
|
2359
2360
|
},
|
|
2360
2361
|
};
|
|
2362
|
+
const defaultProjectEvalRuntimeFactory = {
|
|
2363
|
+
create: (options) => defaultPluginEvalRuntimeFactory.create(options),
|
|
2364
|
+
identify: async (options) => {
|
|
2365
|
+
const environment = process.env;
|
|
2366
|
+
const target = await resolveProviderRuntimeTarget({
|
|
2367
|
+
configRoot: options.configRoot,
|
|
2368
|
+
cwd: options.cwd,
|
|
2369
|
+
environment,
|
|
2370
|
+
...(options.model === undefined ? {} : { model: options.model }),
|
|
2371
|
+
includeSettings: true,
|
|
2372
|
+
includeProjectSettings: false,
|
|
2373
|
+
});
|
|
2374
|
+
return {
|
|
2375
|
+
providerId: target.providerId,
|
|
2376
|
+
profileId: target.profileId,
|
|
2377
|
+
protocol: target.protocol,
|
|
2378
|
+
endpoint: target.baseUrl,
|
|
2379
|
+
modelId: target.modelId,
|
|
2380
|
+
};
|
|
2381
|
+
},
|
|
2382
|
+
};
|
|
2361
2383
|
const defaultPluginEvalJudge = {
|
|
2362
2384
|
vote: async ({ criteria, focus, baseline, model, signal }) => {
|
|
2363
2385
|
const environment = process.env;
|
|
@@ -2600,7 +2622,8 @@ export function createDefaultDependencies(entrypoint = fileURLToPath(import.meta
|
|
|
2600
2622
|
judge: defaultPluginEvalJudge,
|
|
2601
2623
|
},
|
|
2602
2624
|
projectEval: {
|
|
2603
|
-
runtimeFactory:
|
|
2625
|
+
runtimeFactory: defaultProjectEvalRuntimeFactory,
|
|
2626
|
+
loadBuildIdentity: () => loadPraxisBuildIdentity(),
|
|
2604
2627
|
version: VERSION,
|
|
2605
2628
|
configRoot: resolveDataPlaneRoot(),
|
|
2606
2629
|
},
|
|
@@ -27,6 +27,17 @@ export interface EvalRuntimeFactoryOptions {
|
|
|
27
27
|
export interface EvalRuntimeFactory {
|
|
28
28
|
create(options: EvalRuntimeFactoryOptions): Promise<EvalRuntime>;
|
|
29
29
|
}
|
|
30
|
+
export type EvalRuntimeFactoryIdentityOptions = Omit<EvalRuntimeFactoryOptions, 'eventSink'>;
|
|
31
|
+
export interface EvalRuntimeIdentityDescriptor {
|
|
32
|
+
providerId: string;
|
|
33
|
+
profileId: string;
|
|
34
|
+
protocol: string;
|
|
35
|
+
endpoint: string;
|
|
36
|
+
modelId: string;
|
|
37
|
+
}
|
|
38
|
+
export interface IdentifiedEvalRuntimeFactory extends EvalRuntimeFactory {
|
|
39
|
+
identify(options: EvalRuntimeFactoryIdentityOptions): Promise<EvalRuntimeIdentityDescriptor>;
|
|
40
|
+
}
|
|
30
41
|
export interface EvalTraceEvent {
|
|
31
42
|
type: string;
|
|
32
43
|
tool?: string;
|
|
@@ -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,18 +21,20 @@ 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;
|
|
27
28
|
version: string;
|
|
28
29
|
model: string | null;
|
|
30
|
+
identity_sha256: `sha256:${string}`;
|
|
29
31
|
};
|
|
30
32
|
candidate: {
|
|
31
33
|
name: string;
|
|
32
34
|
source_path: string;
|
|
33
35
|
version: string;
|
|
34
36
|
model: string | null;
|
|
37
|
+
identity_sha256: `sha256:${string}`;
|
|
35
38
|
};
|
|
36
39
|
comparable_run_count: number;
|
|
37
40
|
passed: boolean;
|
|
@@ -43,6 +46,8 @@ export interface ProjectEvalComparisonResult {
|
|
|
43
46
|
}[];
|
|
44
47
|
metrics: {
|
|
45
48
|
pass_rate: ProjectEvalComparisonMetric<number>;
|
|
49
|
+
verification_pass_rate: ProjectEvalComparisonMetric<number | null>;
|
|
50
|
+
task_risk_pass_rate: Record<ProjectEvalRisk, ProjectEvalComparisonMetric<number | null>>;
|
|
46
51
|
safety_pass_rate: ProjectEvalComparisonMetric<number | null>;
|
|
47
52
|
average_turns: ProjectEvalComparisonMetric<number>;
|
|
48
53
|
input_tokens: ProjectEvalComparisonMetric<number | null>;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
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
|
+
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';
|
|
4
7
|
const MAX_AGGREGATE_BYTES = 8 * 1024 * 1024;
|
|
5
8
|
const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
|
|
6
9
|
export const PROJECT_EVAL_COMPARE_HELP = `Usage: praxis eval compare [options]
|
|
@@ -106,12 +109,6 @@ function objectField(value, path) {
|
|
|
106
109
|
fail(path, 'expected an object');
|
|
107
110
|
return value;
|
|
108
111
|
}
|
|
109
|
-
function optionalGroup(value, keys, path) {
|
|
110
|
-
const present = keys.filter((key) => value[key] !== undefined);
|
|
111
|
-
if (present.length !== 0 && present.length !== keys.length)
|
|
112
|
-
fail(path, `fields must be all present or all absent: ${keys.join(', ')}`);
|
|
113
|
-
return present.length === keys.length;
|
|
114
|
-
}
|
|
115
112
|
function usageField(value, path) {
|
|
116
113
|
if (value === null)
|
|
117
114
|
return null;
|
|
@@ -130,17 +127,117 @@ function usageField(value, path) {
|
|
|
130
127
|
fail(path, 'inputTokens and outputTokens are required');
|
|
131
128
|
return usage;
|
|
132
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
|
+
}
|
|
133
205
|
function validateRun(value, index) {
|
|
134
206
|
const path = `runs[${index}]`;
|
|
135
207
|
const run = objectField(value, path);
|
|
208
|
+
if (run.schema_version !== '1.2')
|
|
209
|
+
fail(`${path}.schema_version`, 'must be "1.2"');
|
|
136
210
|
stringField(run.case, `${path}.case`);
|
|
137
211
|
if (!IDENTIFIER.test(run.case))
|
|
138
212
|
fail(`${path}.case`, 'unsafe case name');
|
|
139
213
|
const runNumber = numberField(run.run, `${path}.run`, true);
|
|
140
214
|
if (runNumber < 1)
|
|
141
215
|
fail(`${path}.run`, 'must be positive');
|
|
142
|
-
stringField(run.model, `${path}.model
|
|
216
|
+
const model = stringField(run.model, `${path}.model`);
|
|
217
|
+
const identity = validateProjectEvalIdentity(run.identity);
|
|
218
|
+
if (model !== identity.model_id)
|
|
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);
|
|
143
238
|
boolField(run.passed, `${path}.passed`);
|
|
239
|
+
if (run.passed !== recomputedPassed)
|
|
240
|
+
fail(`${path}.passed`, 'does not match compact checks');
|
|
144
241
|
if (run.score !== 0 && run.score !== 1)
|
|
145
242
|
fail(`${path}.score`, 'must be 0 or 1');
|
|
146
243
|
if (run.score !== (run.passed ? 1 : 0))
|
|
@@ -158,16 +255,24 @@ function validateRun(value, index) {
|
|
|
158
255
|
fail(`${path}.termination`, 'invalid termination');
|
|
159
256
|
stringField(run.error, `${path}.error`, true);
|
|
160
257
|
stringField(run.artifact_dir, `${path}.artifact_dir`);
|
|
161
|
-
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
+
};
|
|
171
276
|
}
|
|
172
277
|
export async function loadProjectEvalAggregate(inputPath, callerCwd = process.cwd()) {
|
|
173
278
|
const sourcePath = resolve(callerCwd, inputPath);
|
|
@@ -190,8 +295,8 @@ export async function loadProjectEvalAggregate(inputPath, callerCwd = process.cw
|
|
|
190
295
|
}
|
|
191
296
|
const aggregate = objectField(value, 'root');
|
|
192
297
|
const data = aggregate;
|
|
193
|
-
if (aggregate.schema_version !== '1.
|
|
194
|
-
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');
|
|
195
300
|
stringField(aggregate.version, 'version');
|
|
196
301
|
stringField(aggregate.start, 'start');
|
|
197
302
|
numberField(aggregate.duration_ms, 'duration_ms');
|
|
@@ -230,6 +335,14 @@ export async function loadProjectEvalAggregate(inputPath, callerCwd = process.cw
|
|
|
230
335
|
if (!Array.isArray(aggregate.runs) || aggregate.runs.length > 100000)
|
|
231
336
|
fail('runs', 'expected a bounded array');
|
|
232
337
|
const runs = aggregate.runs.map(validateRun);
|
|
338
|
+
for (const [index, run] of runs.entries())
|
|
339
|
+
if (run.identity.runtime.praxis_version !== aggregate.version)
|
|
340
|
+
fail(`runs[${index}].identity.runtime.praxis_version`, 'must match aggregate version');
|
|
341
|
+
const expectedAggregateModel = runs.length > 0 && runs.every((run) => run.model === runs[0]?.model)
|
|
342
|
+
? (runs[0]?.model ?? null)
|
|
343
|
+
: null;
|
|
344
|
+
if (aggregate.model !== expectedAggregateModel)
|
|
345
|
+
fail('model', 'does not match completed run identity models');
|
|
233
346
|
if (data.run_count !== runs.length)
|
|
234
347
|
fail('run_count', 'does not match runs length');
|
|
235
348
|
if (data.completed_run_count > data.planned_run_count ||
|
|
@@ -285,6 +398,11 @@ export async function loadProjectEvalAggregate(inputPath, callerCwd = process.cw
|
|
|
285
398
|
keys.add(key);
|
|
286
399
|
caseNames.add(run.case);
|
|
287
400
|
}
|
|
401
|
+
validateProjectEvalAggregateIdentity(aggregate.identity_sha256, runs.map((run) => ({
|
|
402
|
+
case: run.case,
|
|
403
|
+
run: run.run,
|
|
404
|
+
identity_sha256: run.identity.identity_sha256,
|
|
405
|
+
})));
|
|
288
406
|
if (!data.partial && caseNames.size !== data.case_count)
|
|
289
407
|
fail('case_count', 'does not match completed run cases');
|
|
290
408
|
if (data.interrupted && !data.partial)
|
|
@@ -294,64 +412,95 @@ export async function loadProjectEvalAggregate(inputPath, callerCwd = process.cw
|
|
|
294
412
|
if (data.partial !==
|
|
295
413
|
(data.interrupted || data.run_count < data.planned_run_count))
|
|
296
414
|
fail('partial', 'does not match interrupted/completed run state');
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
run
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
run.permission_decisions
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
numberField(
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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');
|
|
355
504
|
}
|
|
356
505
|
return {
|
|
357
506
|
aggregate: { ...aggregate, runs },
|
|
@@ -393,6 +542,13 @@ export function compareProjectEvalAggregates(baseline, candidate, baselineName,
|
|
|
393
542
|
throw new Error('Aggregates have different comparable run sets');
|
|
394
543
|
if (leftRuns.length === 0)
|
|
395
544
|
throw new Error('Comparison requires at least one completed run');
|
|
545
|
+
for (let index = 0; index < leftRuns.length; index += 1) {
|
|
546
|
+
const leftRun = leftRuns[index];
|
|
547
|
+
const rightRun = rightRuns[index];
|
|
548
|
+
if (!leftRun || !rightRun)
|
|
549
|
+
continue;
|
|
550
|
+
assertProjectEvalIdentitiesComparable(leftRun.identity, rightRun.identity, `Identity mismatch for (${leftRun.case}, ${leftRun.run})`);
|
|
551
|
+
}
|
|
396
552
|
const regressions = rightRuns.flatMap((run, index) => leftRuns[index]?.passed && !run.passed
|
|
397
553
|
? [
|
|
398
554
|
{
|
|
@@ -410,6 +566,21 @@ export function compareProjectEvalAggregates(baseline, candidate, baselineName,
|
|
|
410
566
|
const rightSafety = safetyKnown
|
|
411
567
|
? rightRuns.filter((run) => run.safety_passed).length / rightRuns.length
|
|
412
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
|
+
};
|
|
413
584
|
const terms = (runs, kind) => runs.filter((run) => kind === 'completed'
|
|
414
585
|
? run.termination === null
|
|
415
586
|
: run.termination === kind).length;
|
|
@@ -418,27 +589,38 @@ export function compareProjectEvalAggregates(baseline, candidate, baselineName,
|
|
|
418
589
|
? metric(leftRuns.reduce((n, r) => n + r.permission_decisions[name], 0), rightRuns.reduce((n, r) => n + r.permission_decisions[name], 0))
|
|
419
590
|
: nullableMetric(null, null);
|
|
420
591
|
const result = {
|
|
421
|
-
schema_version: '1.
|
|
592
|
+
schema_version: '1.2',
|
|
422
593
|
baseline: {
|
|
423
594
|
name: baselineName,
|
|
424
595
|
source_path: baseline.sourcePath,
|
|
425
596
|
version: left.version,
|
|
426
597
|
model: left.model,
|
|
598
|
+
identity_sha256: left.identity_sha256,
|
|
427
599
|
},
|
|
428
600
|
candidate: {
|
|
429
601
|
name: candidateName,
|
|
430
602
|
source_path: candidate.sourcePath,
|
|
431
603
|
version: right.version,
|
|
432
604
|
model: right.model,
|
|
605
|
+
identity_sha256: right.identity_sha256,
|
|
433
606
|
},
|
|
434
607
|
comparable_run_count: leftRuns.length,
|
|
435
608
|
passed: regressions.length === 0 &&
|
|
436
609
|
right.pass_rate >= left.pass_rate &&
|
|
437
610
|
safetyKnown &&
|
|
438
|
-
(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),
|
|
439
616
|
regressions,
|
|
440
617
|
metrics: {
|
|
441
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
|
+
])),
|
|
442
624
|
safety_pass_rate: nullableMetric(leftSafety, rightSafety),
|
|
443
625
|
average_turns: metric(avg(leftRuns.map((r) => r.turns)), avg(rightRuns.map((r) => r.turns))),
|
|
444
626
|
input_tokens: token('input_tokens'),
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { EvalRuntimeIdentityDescriptor } from './eval-contract.js';
|
|
2
|
+
import type { ProjectEvalCase } from './project-eval-schema.js';
|
|
3
|
+
import type { FileManifest } from './project-eval-workspace.js';
|
|
4
|
+
import { type PraxisBuildIdentity } from '../platform/praxis-build-identity.js';
|
|
5
|
+
export declare const PROJECT_EVAL_IDENTITY_SCHEMA_VERSION: "1.1";
|
|
6
|
+
export type ProjectEvalIdentitySchemaVersion = typeof PROJECT_EVAL_IDENTITY_SCHEMA_VERSION;
|
|
7
|
+
export type IdentityDigest = `sha256:${string}`;
|
|
8
|
+
export interface ProjectEvalRuntimeIdentity {
|
|
9
|
+
engine: 'praxis';
|
|
10
|
+
praxis_version: string;
|
|
11
|
+
node_version: string;
|
|
12
|
+
platform: string;
|
|
13
|
+
architecture: string;
|
|
14
|
+
build: PraxisBuildIdentity;
|
|
15
|
+
runtime_sha256: IdentityDigest;
|
|
16
|
+
}
|
|
17
|
+
export interface ProjectEvalIdentity {
|
|
18
|
+
schema_version: ProjectEvalIdentitySchemaVersion;
|
|
19
|
+
provider_id: string;
|
|
20
|
+
profile_id: string;
|
|
21
|
+
protocol: string;
|
|
22
|
+
endpoint_sha256: IdentityDigest;
|
|
23
|
+
model_id: string;
|
|
24
|
+
configuration_sha256: IdentityDigest;
|
|
25
|
+
tools_sha256: IdentityDigest;
|
|
26
|
+
prompt_sha256: IdentityDigest;
|
|
27
|
+
corpus_sha256: IdentityDigest;
|
|
28
|
+
runtime: ProjectEvalRuntimeIdentity;
|
|
29
|
+
identity_sha256: IdentityDigest;
|
|
30
|
+
}
|
|
31
|
+
export interface CreateProjectEvalIdentityInput {
|
|
32
|
+
provider: EvalRuntimeIdentityDescriptor;
|
|
33
|
+
case: ProjectEvalCase;
|
|
34
|
+
sourceBefore: FileManifest;
|
|
35
|
+
effectiveTools: readonly string[];
|
|
36
|
+
runVerification: boolean;
|
|
37
|
+
praxisVersion: string;
|
|
38
|
+
nodeVersion?: string;
|
|
39
|
+
platform?: string;
|
|
40
|
+
architecture?: string;
|
|
41
|
+
buildIdentity: PraxisBuildIdentity;
|
|
42
|
+
}
|
|
43
|
+
export interface AggregateIdentityRun {
|
|
44
|
+
case: string;
|
|
45
|
+
run: number;
|
|
46
|
+
identity_sha256: IdentityDigest;
|
|
47
|
+
}
|
|
48
|
+
export declare function createProjectEvalIdentity(input: CreateProjectEvalIdentityInput): ProjectEvalIdentity;
|
|
49
|
+
export declare function validateProjectEvalIdentity(value: unknown): ProjectEvalIdentity;
|
|
50
|
+
export declare function computeProjectEvalAggregateIdentity(runs: readonly AggregateIdentityRun[]): IdentityDigest;
|
|
51
|
+
export declare function validateProjectEvalAggregateIdentity(value: unknown, runs: readonly AggregateIdentityRun[]): IdentityDigest;
|
|
52
|
+
export declare function firstProjectEvalIdentityMismatch(left: ProjectEvalIdentity, right: ProjectEvalIdentity): string | null;
|
|
53
|
+
export declare function assertProjectEvalIdentitiesComparable(left: ProjectEvalIdentity, right: ProjectEvalIdentity, context?: string): void;
|
|
54
|
+
//# sourceMappingURL=project-eval-identity.d.ts.map
|