@velum-labs/routekit-eval-core 1.0.26 → 1.1.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.
@@ -0,0 +1,36 @@
1
+ import type { RoutingBasisV3 } from "@velum-labs/routekit-eval-contracts";
2
+ export type RoutingBasisStructureFindingV3 = Readonly<{
3
+ severity: "hard" | "warning";
4
+ code: string;
5
+ dimensionIds: readonly string[];
6
+ detail: string;
7
+ }>;
8
+ export type RoutingBasisStructureReportV3 = Readonly<{
9
+ basisId: string;
10
+ basisDigest: string;
11
+ eligible: boolean;
12
+ dimensionCount: number;
13
+ definitionBytes: number;
14
+ findings: readonly RoutingBasisStructureFindingV3[];
15
+ }>;
16
+ export type RoutingBasisCandidateMetricsV3 = Readonly<{
17
+ basis: RoutingBasisV3;
18
+ structure: RoutingBasisStructureReportV3;
19
+ activeMae: number;
20
+ unknownMae: number;
21
+ referenceUnknownRate: number;
22
+ activeF1: number;
23
+ topDimensionAgreement: number;
24
+ provisional?: boolean;
25
+ }>;
26
+ export type RoutingBasisCandidateSelectionV3 = Readonly<{
27
+ ranked: readonly Readonly<RoutingBasisCandidateMetricsV3 & {
28
+ selectionScore: number;
29
+ pareto: boolean;
30
+ }>[];
31
+ selectedBasisId?: string;
32
+ paretoBasisIds: readonly string[];
33
+ }>;
34
+ export declare function inspectRoutingBasisStructureV3(basis: RoutingBasisV3, aliasSimilarityThreshold?: number): RoutingBasisStructureReportV3;
35
+ export declare function routingBasisSelectionScoreV3(input: Pick<RoutingBasisCandidateMetricsV3, "activeMae" | "unknownMae" | "referenceUnknownRate">): number;
36
+ export declare function selectRoutingBasisCandidatesV3(candidates: readonly RoutingBasisCandidateMetricsV3[]): RoutingBasisCandidateSelectionV3;
@@ -0,0 +1,118 @@
1
+ const CATCH_ALL = /\b(other|misc(?:ellaneous)?|general|shared|common|everything else)\b/iu;
2
+ function tokens(dimension) {
3
+ const text = [dimension.name, dimension.description, ...dimension.inclusions]
4
+ .join(" ")
5
+ .toLocaleLowerCase("en-US");
6
+ return new Set(text.split(/[^a-z0-9]+/u).filter((token) => token.length > 2));
7
+ }
8
+ function jaccard(left, right) {
9
+ const intersection = [...left].filter((token) => right.has(token)).length;
10
+ const union = new Set([...left, ...right]).size;
11
+ return union === 0 ? 0 : intersection / union;
12
+ }
13
+ export function inspectRoutingBasisStructureV3(basis, aliasSimilarityThreshold = 0.58) {
14
+ const findings = [];
15
+ for (const dimension of basis.dimensions) {
16
+ if (CATCH_ALL.test(`${dimension.id} ${dimension.name} ${dimension.description}`))
17
+ findings.push({
18
+ severity: "hard",
19
+ code: "catch_all_dimension",
20
+ dimensionIds: [dimension.id],
21
+ detail: "dimension appears to define an open-ended catch-all responsibility"
22
+ });
23
+ if (dimension.inclusions.length === 0 ||
24
+ dimension.exclusions.length === 0 ||
25
+ dimension.boundaryRules.length === 0)
26
+ findings.push({
27
+ severity: "hard",
28
+ code: "empty_semantics",
29
+ dimensionIds: [dimension.id],
30
+ detail: "dimension lacks inclusions, exclusions, or boundary rules"
31
+ });
32
+ if (dimension.repositoryFacets.componentIds.length +
33
+ dimension.repositoryFacets.pathAnchors.length +
34
+ dimension.repositoryFacets.symbolAnchors.length ===
35
+ 0 &&
36
+ dimension.codeSummaries.length === 0)
37
+ findings.push({
38
+ severity: "hard",
39
+ code: "unsupported_dimension",
40
+ dimensionIds: [dimension.id],
41
+ detail: "dimension has no repository evidence anchors"
42
+ });
43
+ }
44
+ for (let leftIndex = 0; leftIndex < basis.dimensions.length; leftIndex += 1) {
45
+ const left = basis.dimensions[leftIndex];
46
+ for (let rightIndex = leftIndex + 1; rightIndex < basis.dimensions.length; rightIndex += 1) {
47
+ const right = basis.dimensions[rightIndex];
48
+ if (left.id.startsWith(`${right.id}-`) || right.id.startsWith(`${left.id}-`))
49
+ findings.push({
50
+ severity: "hard",
51
+ code: "parent_child_pair",
52
+ dimensionIds: [left.id, right.id],
53
+ detail: "dimensions mix parent and child responsibilities at one routing level"
54
+ });
55
+ if (jaccard(tokens(left), tokens(right)) >= aliasSimilarityThreshold)
56
+ findings.push({
57
+ severity: "hard",
58
+ code: "near_alias_dimensions",
59
+ dimensionIds: [left.id, right.id],
60
+ detail: `semantic-token similarity meets the ${String(aliasSimilarityThreshold)} rejection threshold`
61
+ });
62
+ }
63
+ }
64
+ return {
65
+ basisId: basis.basisId,
66
+ basisDigest: basis.basisDigest,
67
+ eligible: !findings.some((finding) => finding.severity === "hard"),
68
+ dimensionCount: basis.dimensions.length,
69
+ definitionBytes: Buffer.byteLength(JSON.stringify(basis.dimensions), "utf8"),
70
+ findings
71
+ };
72
+ }
73
+ export function routingBasisSelectionScoreV3(input) {
74
+ return input.activeMae + 0.5 * input.unknownMae + 0.15 * input.referenceUnknownRate;
75
+ }
76
+ function dominates(left, right) {
77
+ const leftValues = [
78
+ left.activeMae,
79
+ left.unknownMae,
80
+ left.structure.dimensionCount,
81
+ left.structure.definitionBytes
82
+ ];
83
+ const rightValues = [
84
+ right.activeMae,
85
+ right.unknownMae,
86
+ right.structure.dimensionCount,
87
+ right.structure.definitionBytes
88
+ ];
89
+ return (leftValues.every((value, index) => value <= rightValues[index]) &&
90
+ leftValues.some((value, index) => value < rightValues[index]));
91
+ }
92
+ export function selectRoutingBasisCandidatesV3(candidates) {
93
+ const eligible = candidates.filter((candidate) => candidate.structure.eligible);
94
+ const pareto = eligible.filter((candidate) => !eligible.some((other) => other !== candidate && dominates(other, candidate)));
95
+ const paretoIds = new Set(pareto.map((candidate) => candidate.basis.basisId));
96
+ const ranked = candidates
97
+ .map((candidate) => ({
98
+ ...candidate,
99
+ selectionScore: candidate.structure.eligible
100
+ ? routingBasisSelectionScoreV3(candidate)
101
+ : Number.POSITIVE_INFINITY,
102
+ pareto: paretoIds.has(candidate.basis.basisId)
103
+ }))
104
+ .sort((left, right) => left.selectionScore - right.selectionScore ||
105
+ left.structure.dimensionCount - right.structure.dimensionCount ||
106
+ left.structure.definitionBytes - right.structure.definitionBytes ||
107
+ left.basis.basisId.localeCompare(right.basis.basisId));
108
+ return {
109
+ ranked,
110
+ ...(ranked.find((candidate) => Number.isFinite(candidate.selectionScore)) === undefined
111
+ ? {}
112
+ : {
113
+ selectedBasisId: ranked.find((candidate) => Number.isFinite(candidate.selectionScore))
114
+ .basis.basisId
115
+ }),
116
+ paretoBasisIds: pareto.map((candidate) => candidate.basis.basisId).sort()
117
+ };
118
+ }
@@ -0,0 +1,20 @@
1
+ import type { DimensionScoreLabelV3, DimensionScorePredictionV3 } from "@velum-labs/routekit-eval-contracts";
2
+ export type ClassificationMetricOptionsV3 = Readonly<{
3
+ activeDimensionThreshold: number;
4
+ unknownThreshold: number;
5
+ }>;
6
+ export type ClassificationMetricsV3 = Readonly<{
7
+ caseCount: number;
8
+ skippedInvalidCount: number;
9
+ activeMae: number;
10
+ activeF1: number;
11
+ topDimensionAgreement: number;
12
+ unknownMae: number;
13
+ unknownBrier: number;
14
+ unknownAccuracy: number;
15
+ falseUnknownRate: number;
16
+ missedUnknownRate: number;
17
+ referenceUnknownRate: number;
18
+ averageActiveDimensionCount: number;
19
+ }>;
20
+ export declare function evaluateClassificationMetricsV3(labels: readonly DimensionScoreLabelV3[], predictions: readonly DimensionScorePredictionV3[], options: ClassificationMetricOptionsV3): ClassificationMetricsV3;
@@ -0,0 +1,83 @@
1
+ function unit(value, label) {
2
+ if (!Number.isFinite(value) || value < 0 || value > 1)
3
+ throw new Error(`${label} must be between zero and one`);
4
+ }
5
+ function top(scores) {
6
+ return [...scores].sort((left, right) => right.score - left.score || left.dimensionId.localeCompare(right.dimensionId))[0]?.dimensionId;
7
+ }
8
+ export function evaluateClassificationMetricsV3(labels, predictions, options) {
9
+ unit(options.activeDimensionThreshold, "active dimension threshold");
10
+ unit(options.unknownThreshold, "unknown threshold");
11
+ const predictionByCase = new Map(predictions.map((prediction) => [prediction.caseId, prediction]));
12
+ if (predictionByCase.size !== predictions.length)
13
+ throw new Error("predictions contain duplicate case IDs");
14
+ let absolute = 0, scoreCount = 0, tp = 0, fp = 0, fn = 0, topAgreement = 0;
15
+ let unknownAbsolute = 0, unknownSquared = 0, unknownCorrect = 0, falseUnknown = 0, missedUnknown = 0, referenceUnknown = 0, activeCount = 0;
16
+ for (const label of labels) {
17
+ const prediction = predictionByCase.get(label.caseId);
18
+ if (prediction === undefined)
19
+ throw new Error(`missing prediction for case ${label.caseId}`);
20
+ if (prediction.basisDigest !== label.basisDigest)
21
+ throw new Error(`basis digest mismatch for case ${label.caseId}`);
22
+ const labelIds = label.scores.map((entry) => entry.dimensionId);
23
+ const predictionIds = prediction.scores.map((entry) => entry.dimensionId);
24
+ if (labelIds.length !== predictionIds.length ||
25
+ labelIds.some((id, index) => predictionIds[index] !== id))
26
+ throw new Error(`dimension mismatch for case ${label.caseId}`);
27
+ for (let index = 0; index < label.scores.length; index += 1) {
28
+ const expected = label.scores[index];
29
+ const actual = prediction.scores[index];
30
+ unit(expected.score, "reference score");
31
+ unit(actual.score, "prediction score");
32
+ absolute += Math.abs(expected.score - actual.score);
33
+ scoreCount += 1;
34
+ const expectedActive = expected.score >= options.activeDimensionThreshold;
35
+ const actualActive = actual.score >= options.activeDimensionThreshold;
36
+ if (expectedActive && actualActive)
37
+ tp += 1;
38
+ else if (!expectedActive && actualActive)
39
+ fp += 1;
40
+ else if (expectedActive)
41
+ fn += 1;
42
+ if (actualActive)
43
+ activeCount += 1;
44
+ }
45
+ if (top(label.scores) === top(prediction.scores))
46
+ topAgreement += 1;
47
+ unit(label.unknownProbability, "reference unknown probability");
48
+ unit(prediction.unknownProbability, "predicted unknown probability");
49
+ const delta = prediction.unknownProbability - label.unknownProbability;
50
+ unknownAbsolute += Math.abs(delta);
51
+ unknownSquared += delta * delta;
52
+ const expectedUnknown = label.unknownProbability >= options.unknownThreshold;
53
+ const actualUnknown = prediction.unknownProbability >= options.unknownThreshold;
54
+ if (expectedUnknown)
55
+ referenceUnknown += 1;
56
+ if (expectedUnknown === actualUnknown)
57
+ unknownCorrect += 1;
58
+ else if (actualUnknown)
59
+ falseUnknown += 1;
60
+ else
61
+ missedUnknown += 1;
62
+ }
63
+ if (predictionByCase.size !== labels.length)
64
+ throw new Error("predictions contain cases absent from the reference set");
65
+ const count = labels.length;
66
+ if (count === 0 || scoreCount === 0)
67
+ throw new Error("classification metrics require at least one case and dimension");
68
+ const f1Denominator = 2 * tp + fp + fn;
69
+ return {
70
+ caseCount: count,
71
+ skippedInvalidCount: 0,
72
+ activeMae: absolute / scoreCount,
73
+ activeF1: f1Denominator === 0 ? 1 : (2 * tp) / f1Denominator,
74
+ topDimensionAgreement: topAgreement / count,
75
+ unknownMae: unknownAbsolute / count,
76
+ unknownBrier: unknownSquared / count,
77
+ unknownAccuracy: unknownCorrect / count,
78
+ falseUnknownRate: falseUnknown / count,
79
+ missedUnknownRate: missedUnknown / count,
80
+ referenceUnknownRate: referenceUnknown / count,
81
+ averageActiveDimensionCount: activeCount / count
82
+ };
83
+ }
@@ -1,2 +1,4 @@
1
1
  export type { EvalEgressOptions } from "./egress.js";
2
2
  export { type RoutingModelAvailability, type RoutingScoreConstraints, type RoutingScoreResult, RoutingScoringError, type RoutingScoringErrorCode, type ScoreRoutingCandidatesInput, scoreRoutingCandidates } from "./routing-score.js";
3
+ export * from "./basis-selection.js";
4
+ export * from "./classification-metrics.js";
@@ -1 +1,3 @@
1
1
  export { RoutingScoringError, scoreRoutingCandidates } from "./routing-score.js";
2
+ export * from "./basis-selection.js";
3
+ export * from "./classification-metrics.js";
package/dist/index.d.ts CHANGED
@@ -1,2 +1,4 @@
1
1
  export type { EvalEgressOptions } from "./egress.js";
2
- export { type RoutingModelAvailability, type RoutingScoreConstraints, type RoutingScoreResult, RoutingScoringError, type RoutingScoringErrorCode, type ScoreRoutingCandidatesInput, scoreRoutingCandidates } from "./routing-score.js";
2
+ export { type RoutingModelAvailability, type RoutingScoreConstraints, type RoutingScoreComposition, type RoutingScoreEvidence, type RoutingScoreResult, RoutingScoringError, type RoutingScoringErrorCode, type ScoreRoutingCandidatesInput, scoreRoutingCandidates } from "./routing-score.js";
3
+ export * from "./basis-selection.js";
4
+ export * from "./classification-metrics.js";
package/dist/index.js CHANGED
@@ -1 +1,3 @@
1
1
  export { RoutingScoringError, scoreRoutingCandidates } from "./routing-score.js";
2
+ export * from "./basis-selection.js";
3
+ export * from "./classification-metrics.js";
@@ -1,4 +1,4 @@
1
- import type { PublishedRoutingActivation, RequestDecomposition, RequestRoutingRequirements, RoutingCandidateDecision, RoutingEndpoint, RoutingObjectivePolicy } from "@velum-labs/routekit-eval-contracts";
1
+ import type { ModelDimensionEvidence, RequestRoutingRequirements, RoutingCandidateDecision, RoutingEndpoint, RoutingObjectivePolicy } from "@velum-labs/routekit-eval-contracts";
2
2
  export type RoutingModelAvailability = Readonly<{
3
3
  model: string;
4
4
  served: boolean;
@@ -14,9 +14,25 @@ export type RoutingScoreConstraints = Readonly<{
14
14
  /** Maximum weighted failure rate for a candidate. */
15
15
  maximumFailureRate?: number;
16
16
  }>;
17
+ /** Version-neutral evidence consumed by deterministic routing. */
18
+ export type RoutingScoreEvidence = Readonly<{
19
+ basisDigest: string;
20
+ dimensionIds: readonly string[];
21
+ candidateModels: readonly string[];
22
+ evidence: readonly ModelDimensionEvidence[];
23
+ }>;
24
+ /** Version-neutral composition consumed by deterministic routing. */
25
+ export type RoutingScoreComposition = Readonly<{
26
+ basisDigest: string;
27
+ weights: readonly Readonly<{
28
+ dimensionId: string;
29
+ weight: number;
30
+ }>[];
31
+ unassignedWeight: number;
32
+ }>;
17
33
  export type ScoreRoutingCandidatesInput = Readonly<{
18
- snapshot: PublishedRoutingActivation;
19
- decomposition: RequestDecomposition;
34
+ routingEvidence: RoutingScoreEvidence;
35
+ composition: RoutingScoreComposition;
20
36
  requirements: RequestRoutingRequirements;
21
37
  objective: RoutingObjectivePolicy;
22
38
  availableModels: readonly RoutingModelAvailability[];
@@ -40,19 +40,19 @@ function compareNumberDescending(left, right) {
40
40
  return right - left;
41
41
  }
42
42
  function activeWeights(input) {
43
- if (input.snapshot.basisDigest !== input.decomposition.basisDigest) {
44
- invalid("dimension definition digest does not match the routing snapshot");
43
+ if (input.routingEvidence.basisDigest !== input.composition.basisDigest) {
44
+ invalid("dimension definition digest does not match the routing evidence");
45
45
  }
46
- const dimensionIds = input.snapshot.dimensions.map((dimension) => dimension.id);
47
- unique(dimensionIds, "snapshot dimension IDs");
48
- unique(input.snapshot.candidateModels, "snapshot candidate models");
46
+ const dimensionIds = input.routingEvidence.dimensionIds;
47
+ unique(dimensionIds, "routing evidence dimension IDs");
48
+ unique(input.routingEvidence.candidateModels, "routing evidence candidate models");
49
49
  if (dimensionIds.length === 0)
50
- invalid("routing snapshot must contain at least one dimension");
51
- if (input.snapshot.candidateModels.length === 0) {
52
- invalid("routing snapshot must contain at least one candidate model");
50
+ invalid("routing evidence must contain at least one dimension");
51
+ if (input.routingEvidence.candidateModels.length === 0) {
52
+ invalid("routing evidence must contain at least one candidate model");
53
53
  }
54
54
  const weights = new Map();
55
- for (const entry of input.decomposition.weights) {
55
+ for (const entry of input.composition.weights) {
56
56
  finiteUnit(entry.weight, `weight for dimension ${entry.dimensionId}`);
57
57
  if (weights.has(entry.dimensionId))
58
58
  invalid(`duplicate decomposition dimension: ${entry.dimensionId}`);
@@ -64,10 +64,10 @@ function activeWeights(input) {
64
64
  if (!weights.has(dimensionId))
65
65
  invalid(`missing decomposition dimension: ${dimensionId}`);
66
66
  }
67
- finiteUnit(input.decomposition.unknownWeight, "unknown weight");
67
+ finiteUnit(input.composition.unassignedWeight, "unassigned weight");
68
68
  const total = [...weights.values()].reduce((sum, weight) => sum + weight, 0);
69
- if (Math.abs(total + input.decomposition.unknownWeight - 1) > VECTOR_TOLERANCE) {
70
- invalid("dimension weights and unknown weight must sum to one");
69
+ if (Math.abs(total + input.composition.unassignedWeight - 1) > VECTOR_TOLERANCE) {
70
+ invalid("dimension weights and unassigned weight must sum to one");
71
71
  }
72
72
  if (total <= VECTOR_TOLERANCE)
73
73
  invalid("routing requires at least one covered dimension");
@@ -81,7 +81,7 @@ function validateInput(input) {
81
81
  const availableIds = input.availableModels.map((model) => model.model);
82
82
  unique(availableIds, "available model IDs");
83
83
  const floors = input.constraints?.minimumDimensionQuality ?? {};
84
- const knownDimensions = new Set(input.snapshot.dimensions.map((dimension) => dimension.id));
84
+ const knownDimensions = new Set(input.routingEvidence.dimensionIds);
85
85
  for (const [dimensionId, floor] of Object.entries(floors)) {
86
86
  if (!knownDimensions.has(dimensionId))
87
87
  invalid(`quality floor refers to unknown dimension: ${dimensionId}`);
@@ -148,7 +148,7 @@ function capabilityReasons(availability, requirements) {
148
148
  function calculateCandidates(input, active) {
149
149
  const availability = new Map(input.availableModels.map((model) => [model.model, model]));
150
150
  const floors = input.constraints?.minimumDimensionQuality ?? {};
151
- return input.snapshot.candidateModels.map((model) => {
151
+ return input.routingEvidence.candidateModels.map((model) => {
152
152
  const reasons = capabilityReasons(availability.get(model), input.requirements);
153
153
  let quality = 0;
154
154
  let failureRate = 0;
@@ -158,7 +158,7 @@ function calculateCandidates(input, active) {
158
158
  let completeDuration = true;
159
159
  let completeCost = true;
160
160
  for (const { dimensionId, weight } of active) {
161
- const cells = input.snapshot.evidence.filter((cell) => cell.model === model && cell.dimensionId === dimensionId);
161
+ const cells = input.routingEvidence.evidence.filter((cell) => cell.model === model && cell.dimensionId === dimensionId);
162
162
  if (cells.length === 0) {
163
163
  reasons.push(`missing_evidence:${dimensionId}`);
164
164
  completeCoreEvidence = false;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,40 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { evaluateClassificationMetricsV3, routingBasisSelectionScoreV3 } from "../index.js";
4
+ const digest = "a".repeat(64);
5
+ test("classification metrics use independent score and separate unknown errors", () => {
6
+ const labels = [
7
+ {
8
+ caseId: "one",
9
+ basisDigest: digest,
10
+ scores: [
11
+ { dimensionId: "a", score: 1 },
12
+ { dimensionId: "b", score: 0 }
13
+ ],
14
+ unknownProbability: 0
15
+ }
16
+ ];
17
+ const predictions = [
18
+ {
19
+ ...labels[0],
20
+ classifierConfigurationDigest: "b".repeat(64),
21
+ scores: [
22
+ { dimensionId: "a", score: 0.8 },
23
+ { dimensionId: "b", score: 0.3 }
24
+ ],
25
+ unknownProbability: 0.2
26
+ }
27
+ ];
28
+ const metrics = evaluateClassificationMetricsV3(labels, predictions, {
29
+ activeDimensionThreshold: 0.25,
30
+ unknownThreshold: 0.3
31
+ });
32
+ assert.ok(Math.abs(metrics.activeMae - 0.25) < 1e-12);
33
+ assert.equal(metrics.activeF1, 2 / 3);
34
+ assert.equal(metrics.topDimensionAgreement, 1);
35
+ assert.equal(metrics.unknownMae, 0.2);
36
+ assert.ok(Math.abs(metrics.unknownBrier - 0.04) < 1e-12);
37
+ });
38
+ test("basis scalar selection score uses the frozen formula", () => {
39
+ assert.equal(routingBasisSelectionScoreV3({ activeMae: 0.1, unknownMae: 0.2, referenceUnknownRate: 0.4 }), 0.26);
40
+ });
@@ -62,9 +62,19 @@ function available(model, overrides = {}) {
62
62
  };
63
63
  }
64
64
  function run(evidence, objective = { kind: "highest-quality" }, options = {}) {
65
+ const routingSnapshot = snapshot(evidence, options.candidateModels);
65
66
  return scoreRoutingCandidates({
66
- snapshot: snapshot(evidence, options.candidateModels),
67
- decomposition: options.requestDecomposition ?? decomposition,
67
+ routingEvidence: {
68
+ basisDigest: routingSnapshot.basisDigest,
69
+ dimensionIds: routingSnapshot.dimensions.map((dimension) => dimension.id),
70
+ candidateModels: routingSnapshot.candidateModels,
71
+ evidence: routingSnapshot.evidence
72
+ },
73
+ composition: {
74
+ basisDigest: (options.requestDecomposition ?? decomposition).basisDigest,
75
+ weights: (options.requestDecomposition ?? decomposition).weights,
76
+ unassignedWeight: (options.requestDecomposition ?? decomposition).unknownWeight
77
+ },
68
78
  requirements,
69
79
  objective,
70
80
  availableModels: options.availableModels ?? models.map((model) => available(model))
@@ -99,8 +109,17 @@ test("hard requirements exclude unserved and incapable candidates before scoring
99
109
  ]
100
110
  });
101
111
  const withTools = scoreRoutingCandidates({
102
- snapshot: snapshot(completeEvidence),
103
- decomposition,
112
+ routingEvidence: {
113
+ basisDigest: snapshot(completeEvidence).basisDigest,
114
+ dimensionIds: snapshot(completeEvidence).dimensions.map((dimension) => dimension.id),
115
+ candidateModels: snapshot(completeEvidence).candidateModels,
116
+ evidence: snapshot(completeEvidence).evidence
117
+ },
118
+ composition: {
119
+ basisDigest: decomposition.basisDigest,
120
+ weights: decomposition.weights,
121
+ unassignedWeight: decomposition.unknownWeight
122
+ },
104
123
  requirements: { ...requirements, requiresTools: true },
105
124
  objective: { kind: "highest-quality" },
106
125
  availableModels: [
@@ -128,8 +147,17 @@ test("unknown numeric basis limits do not claim a served model is incapable", ()
128
147
  test("missing evidence and per-dimension quality floors fail candidates closed", () => {
129
148
  const missingLastCell = completeEvidence.filter((entry) => !(entry.model === models[0] && entry.dimensionId === "docs"));
130
149
  const result = scoreRoutingCandidates({
131
- snapshot: snapshot(missingLastCell),
132
- decomposition,
150
+ routingEvidence: {
151
+ basisDigest: snapshot(missingLastCell).basisDigest,
152
+ dimensionIds: snapshot(missingLastCell).dimensions.map((dimension) => dimension.id),
153
+ candidateModels: snapshot(missingLastCell).candidateModels,
154
+ evidence: snapshot(missingLastCell).evidence
155
+ },
156
+ composition: {
157
+ basisDigest: decomposition.basisDigest,
158
+ weights: decomposition.weights,
159
+ unassignedWeight: decomposition.unknownWeight
160
+ },
133
161
  requirements,
134
162
  objective: { kind: "highest-quality" },
135
163
  availableModels: models.map((model) => available(model)),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@velum-labs/routekit-eval-core",
3
3
  "private": false,
4
- "version": "1.0.26",
4
+ "version": "1.1.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/velum-labs/routekit.git",
@@ -31,8 +31,8 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "effect": "4.0.0-rc.108",
34
- "@velum-labs/routekit-eval-contracts": "1.0.26",
35
- "@velum-labs/routekit-runtime": "1.0.26"
34
+ "@velum-labs/routekit-eval-contracts": "1.1.0",
35
+ "@velum-labs/routekit-runtime": "1.1.0"
36
36
  },
37
37
  "keywords": [
38
38
  "routekit",