@velum-labs/routekit-eval-setup 1.0.6 → 1.0.8

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.
@@ -127,6 +127,72 @@ export function selectProjectAuthoringSourceFiles(input) {
127
127
  const DimensionsOutput = Schema.Struct({
128
128
  dimensions: Schema.Array(WorkloadDimension)
129
129
  });
130
+ function schemaRecord(value) {
131
+ return typeof value === "object" && value !== null && !Array.isArray(value)
132
+ ? value
133
+ : undefined;
134
+ }
135
+ /**
136
+ * Anthropic removes unsupported structured-output constraints from its wire
137
+ * schema. Enforce those deferred constraints against the decoded response so
138
+ * authoring validation remains provider-independent.
139
+ */
140
+ function assertDeferredSchemaConstraints(schema, value, path = "$") {
141
+ const record = schemaRecord(schema);
142
+ if (record === undefined)
143
+ return;
144
+ if (typeof value === "number") {
145
+ if (typeof record.minimum === "number" && value < record.minimum) {
146
+ throw new Error(`${path} must be greater than or equal to ${String(record.minimum)}`);
147
+ }
148
+ if (typeof record.maximum === "number" && value > record.maximum) {
149
+ throw new Error(`${path} must be less than or equal to ${String(record.maximum)}`);
150
+ }
151
+ if (typeof record.exclusiveMinimum === "number" && value <= record.exclusiveMinimum) {
152
+ throw new Error(`${path} must be greater than ${String(record.exclusiveMinimum)}`);
153
+ }
154
+ if (typeof record.exclusiveMaximum === "number" && value >= record.exclusiveMaximum) {
155
+ throw new Error(`${path} must be less than ${String(record.exclusiveMaximum)}`);
156
+ }
157
+ if (typeof record.multipleOf === "number" &&
158
+ record.multipleOf !== 0 &&
159
+ Math.abs(value / record.multipleOf - Math.round(value / record.multipleOf)) >
160
+ Number.EPSILON * Math.max(1, Math.abs(value / record.multipleOf)) * 8) {
161
+ throw new Error(`${path} must be a multiple of ${String(record.multipleOf)}`);
162
+ }
163
+ }
164
+ if (typeof value === "string") {
165
+ const length = [...value].length;
166
+ if (typeof record.minLength === "number" && length < record.minLength) {
167
+ throw new Error(`${path} must contain at least ${String(record.minLength)} characters`);
168
+ }
169
+ if (typeof record.maxLength === "number" && length > record.maxLength) {
170
+ throw new Error(`${path} must contain at most ${String(record.maxLength)} characters`);
171
+ }
172
+ }
173
+ if (Array.isArray(value)) {
174
+ if (typeof record.minItems === "number" &&
175
+ record.minItems > 1 &&
176
+ value.length < record.minItems) {
177
+ throw new Error(`${path} must contain at least ${String(record.minItems)} items`);
178
+ }
179
+ if (typeof record.maxItems === "number" && value.length > record.maxItems) {
180
+ throw new Error(`${path} must contain at most ${String(record.maxItems)} items`);
181
+ }
182
+ for (const [index, item] of value.entries()) {
183
+ assertDeferredSchemaConstraints(record.items, item, `${path}[${String(index)}]`);
184
+ }
185
+ }
186
+ const valueRecord = schemaRecord(value);
187
+ const properties = schemaRecord(record.properties);
188
+ if (valueRecord !== undefined && properties !== undefined) {
189
+ for (const [key, propertySchema] of Object.entries(properties)) {
190
+ if (Object.hasOwn(valueRecord, key)) {
191
+ assertDeferredSchemaConstraints(propertySchema, valueRecord[key], `${path}.${key}`);
192
+ }
193
+ }
194
+ }
195
+ }
130
196
  const DIMENSIONS_JSON_SCHEMA = {
131
197
  type: "object",
132
198
  additionalProperties: false,
@@ -350,11 +416,14 @@ export const makeEvalProjectAuthor = Effect.gen(function* () {
350
416
  });
351
417
  const decoded = yield* Schema.decodeUnknownEffect(DimensionsOutput)(yield* parseJson("authoring-dimensions", text)).pipe(Effect.mapError((cause) => failure("authoring-dimensions", "dimension proposal failed validation", cause)));
352
418
  yield* Effect.try({
353
- try: () => assertRoutingBasis({
354
- version: 2,
355
- basisDigest: "authoring-validation",
356
- dimensions: decoded.dimensions
357
- }),
419
+ try: () => {
420
+ assertDeferredSchemaConstraints(DIMENSIONS_JSON_SCHEMA, decoded);
421
+ assertRoutingBasis({
422
+ version: 2,
423
+ basisDigest: "authoring-validation",
424
+ dimensions: decoded.dimensions
425
+ });
426
+ },
358
427
  catch: (cause) => failure("authoring-dimensions", "dimension proposal failed validation", cause)
359
428
  });
360
429
  return decoded.dimensions;
@@ -377,6 +446,10 @@ export const makeEvalProjectAuthor = Effect.gen(function* () {
377
446
  maximumOutputTokens: 16_384
378
447
  });
379
448
  const suite = yield* Schema.decodeUnknownEffect(EvalDimensionSuiteSchema)(yield* parseJson("authoring-evaluations", text)).pipe(Effect.mapError((cause) => failure("authoring-evaluations", `suite for ${JSON.stringify(dimension.id)} failed validation`, cause)));
449
+ yield* Effect.try({
450
+ try: () => assertDeferredSchemaConstraints(SUITE_JSON_SCHEMA, suite),
451
+ catch: (cause) => failure("authoring-evaluations", `suite for ${JSON.stringify(dimension.id)} failed validation`, cause)
452
+ });
380
453
  if (suite.dimensionId !== dimension.id ||
381
454
  suite.cases.length !== EVAL_AUTHORING_CASES_PER_DIMENSION ||
382
455
  new Set(suite.cases.map((testCase) => testCase.id)).size !== suite.cases.length) {
@@ -399,6 +472,10 @@ export const makeEvalProjectAuthor = Effect.gen(function* () {
399
472
  maximumOutputTokens: 16_384
400
473
  });
401
474
  const decompositionBenchmark = yield* Schema.decodeUnknownEffect(EvalDecompositionBenchmarkSchema)(yield* parseJson("authoring-evaluations", decompositionText)).pipe(Effect.mapError((cause) => failure("authoring-evaluations", "decomposition benchmark failed validation", cause)));
475
+ yield* Effect.try({
476
+ try: () => assertDeferredSchemaConstraints(decompositionBenchmarkJsonSchema(dimensionIds), decompositionBenchmark),
477
+ catch: (cause) => failure("authoring-evaluations", "decomposition benchmark failed validation", cause)
478
+ });
402
479
  for (const benchmarkCase of decompositionBenchmark.cases) {
403
480
  yield* Effect.try({
404
481
  try: () => assertDecompositionResult(benchmarkCase.expected, input.basis),
@@ -419,6 +496,10 @@ export const makeEvalProjectAuthor = Effect.gen(function* () {
419
496
  maximumOutputTokens: 16_384
420
497
  });
421
498
  const compositionSuite = yield* Schema.decodeUnknownEffect(EvalCompositionSuiteSchema)(yield* parseJson("authoring-evaluations", compositionText)).pipe(Effect.mapError((cause) => failure("authoring-evaluations", "composition benchmark failed validation", cause)));
499
+ yield* Effect.try({
500
+ try: () => assertDeferredSchemaConstraints(compositionSuiteJsonSchema(dimensionIds), compositionSuite),
501
+ catch: (cause) => failure("authoring-evaluations", "composition benchmark failed validation", cause)
502
+ });
422
503
  for (const compositionCase of compositionSuite.cases) {
423
504
  yield* Effect.try({
424
505
  try: () => assertDecompositionResult(compositionCase.decomposition, input.basis),
@@ -6,7 +6,8 @@ import test from "node:test";
6
6
  import { layer as NodeServicesLayer } from "@effect/platform-node/NodeServices";
7
7
  import { Effect, Layer } from "effect";
8
8
  import { EvalProjectAuthoringError } from "../errors.js";
9
- import { EvalAuthoringTransport, EvalProjectAuthor, EvalProjectAuthorLive, readProjectAuthoringSources } from "../project-authoring.js";
9
+ import { EVAL_AUTHORING_CASES_PER_DIMENSION, EvalAuthoringTransport, EvalProjectAuthor, EvalProjectAuthorLive, readProjectAuthoringSources } from "../project-authoring.js";
10
+ import { EVAL_PROJECT_VERSION } from "../project-contracts.js";
10
11
  const withRepository = async (use) => {
11
12
  const root = await mkdtemp(path.join(os.tmpdir(), "routekit-author-sources-"));
12
13
  const outside = await mkdtemp(path.join(os.tmpdir(), "routekit-author-outside-"));
@@ -52,6 +53,81 @@ const proposeDimensions = (root, output, requests = []) => Effect.runPromise(Eff
52
53
  return JSON.stringify(output);
53
54
  })
54
55
  }))), Layer.provide(NodeServicesLayer)))));
56
+ const basis = {
57
+ version: 2,
58
+ basisDigest: "basis-digest",
59
+ dimensions: dimensions(5)
60
+ };
61
+ const dimensionCases = () => Array.from({ length: EVAL_AUTHORING_CASES_PER_DIMENSION }, (_, index) => ({
62
+ id: `dimension-case-${String(index + 1)}`,
63
+ prompt: `Answer production request ${String(index + 1)}.`,
64
+ context: "Reference context.",
65
+ rubric: "State the expected production behavior."
66
+ }));
67
+ const dimensionSuite = (dimensionId = basis.dimensions[0].id) => ({
68
+ version: EVAL_PROJECT_VERSION,
69
+ dimensionId,
70
+ maximumOutputTokens: 256,
71
+ cases: dimensionCases()
72
+ });
73
+ const decompositionWeights = () => basis.dimensions.map((dimension) => ({
74
+ dimensionId: dimension.id,
75
+ weight: 1 / basis.dimensions.length
76
+ }));
77
+ const decompositionBenchmark = () => ({
78
+ maximumVectorL1Error: 0.25,
79
+ cases: Array.from({ length: EVAL_AUTHORING_CASES_PER_DIMENSION }, (_, index) => ({
80
+ id: `decomposition-case-${String(index + 1)}`,
81
+ request: `Classify production request ${String(index + 1)}.`,
82
+ expected: {
83
+ weights: decompositionWeights(),
84
+ unknownWeight: 0
85
+ }
86
+ }))
87
+ });
88
+ const compositionSuite = () => ({
89
+ maximumOutputTokens: 256,
90
+ minimumWinnerScoreGap: 0.05,
91
+ minimumWinnerAgreement: 0.8,
92
+ cases: Array.from({ length: EVAL_AUTHORING_CASES_PER_DIMENSION }, (_, index) => ({
93
+ id: `composition-case-${String(index + 1)}`,
94
+ prompt: `Compose production response ${String(index + 1)}.`,
95
+ context: "Reference context.",
96
+ rubric: "State the expected composed production behavior.",
97
+ decomposition: {
98
+ weights: decompositionWeights(),
99
+ unknownWeight: 0
100
+ },
101
+ requirements: {
102
+ endpoint: "chat",
103
+ requiresTools: false,
104
+ requiresVision: false,
105
+ inputTokens: 128,
106
+ maxOutputTokens: 256
107
+ }
108
+ }))
109
+ });
110
+ const proposeEvaluations = (root, outputs) => Effect.runPromise(Effect.gen(function* () {
111
+ const author = yield* EvalProjectAuthor;
112
+ return yield* author.proposeEvaluations({
113
+ operationId: "eng-833",
114
+ repositoryRoot: root,
115
+ sourceInventory: ["source.md"],
116
+ configuration,
117
+ basis
118
+ });
119
+ }).pipe(Effect.provide(EvalProjectAuthorLive.pipe(Layer.provide(Layer.succeed(EvalAuthoringTransport, EvalAuthoringTransport.of({
120
+ complete: (input) => Effect.sync(() => {
121
+ if (input.schemaName === "routekit_dimension_suite") {
122
+ const dimension = basis.dimensions.find((candidate) => input.operationId.endsWith(`:${candidate.id}`));
123
+ return JSON.stringify(outputs.suite ?? dimensionSuite(dimension?.id ?? basis.dimensions[0].id));
124
+ }
125
+ if (input.schemaName === "routekit_decomposition_benchmark") {
126
+ return JSON.stringify(outputs.decomposition ?? decompositionBenchmark());
127
+ }
128
+ return JSON.stringify(outputs.composition ?? compositionSuite());
129
+ })
130
+ }))), Layer.provide(NodeServicesLayer)))));
55
131
  const collectSchemaNumberKeyword = (value, keyword) => {
56
132
  if (typeof value !== "object" || value === null)
57
133
  return [];
@@ -142,3 +218,43 @@ test("dimension authoring enforces routing basis counts after structured output
142
218
  });
143
219
  });
144
220
  });
221
+ test("evaluation authoring enforces Anthropic-deferred bounds after parsing", async () => {
222
+ await withRepository(async ({ root }) => {
223
+ for (const maximumOutputTokens of [0, 16_385]) {
224
+ await assert.rejects(proposeEvaluations(root, {
225
+ suite: { ...dimensionSuite(), maximumOutputTokens }
226
+ }), (error) => {
227
+ assert.ok(error instanceof EvalProjectAuthoringError);
228
+ assert.match(error.detail, /suite .* failed validation/u);
229
+ assert.match(String(error.cause), /maximumOutputTokens/u);
230
+ return true;
231
+ });
232
+ }
233
+ await assert.rejects(proposeEvaluations(root, {
234
+ suite: {
235
+ ...dimensionSuite(),
236
+ cases: dimensionCases().slice(0, EVAL_AUTHORING_CASES_PER_DIMENSION - 1)
237
+ }
238
+ }), (error) => {
239
+ assert.ok(error instanceof EvalProjectAuthoringError);
240
+ assert.match(String(error.cause), /cases must contain at least 20 items/u);
241
+ return true;
242
+ });
243
+ await assert.rejects(proposeEvaluations(root, {
244
+ decomposition: { ...decompositionBenchmark(), maximumVectorL1Error: 3 }
245
+ }), (error) => {
246
+ assert.ok(error instanceof EvalProjectAuthoringError);
247
+ assert.equal(error.detail, "decomposition benchmark failed validation");
248
+ assert.match(String(error.cause), /maximumVectorL1Error/u);
249
+ return true;
250
+ });
251
+ await assert.rejects(proposeEvaluations(root, {
252
+ composition: { ...compositionSuite(), minimumWinnerAgreement: 2 }
253
+ }), (error) => {
254
+ assert.ok(error instanceof EvalProjectAuthoringError);
255
+ assert.equal(error.detail, "composition benchmark failed validation");
256
+ assert.match(String(error.cause), /minimumWinnerAgreement/u);
257
+ return true;
258
+ });
259
+ });
260
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@velum-labs/routekit-eval-setup",
3
3
  "private": false,
4
- "version": "1.0.6",
4
+ "version": "1.0.8",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/velum-labs/routekit.git",
@@ -32,8 +32,8 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "effect": "4.0.0-rc.108",
35
- "@velum-labs/routekit-eval-contracts": "1.0.6",
36
- "@velum-labs/routekit-runtime": "1.0.6"
35
+ "@velum-labs/routekit-eval-contracts": "1.0.8",
36
+ "@velum-labs/routekit-runtime": "1.0.8"
37
37
  },
38
38
  "keywords": [
39
39
  "routekit",