@ikon85/agent-workflow-kit 0.44.2 → 0.45.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.
Files changed (64) hide show
  1. package/.agents/skills/audit-skills/SKILL.md +7 -4
  2. package/.agents/skills/code-review/SKILL.md +7 -4
  3. package/.agents/skills/codebase-design/DESIGN-IT-TWICE.md +7 -4
  4. package/.agents/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +7 -4
  5. package/.agents/skills/improve-codebase-architecture/SKILL.md +7 -4
  6. package/.agents/skills/orchestrate-wave/SKILL.md +1 -1
  7. package/.agents/skills/orchestrate-wave/references/dispatch-subagents.md +9 -0
  8. package/.agents/skills/orchestrate-wave/references/dispatch-workflow.md +9 -0
  9. package/.agents/skills/research/SKILL.md +7 -4
  10. package/.agents/skills/to-issues/SKILL.md +25 -4
  11. package/.claude/skills/audit-skills/SKILL.md +7 -4
  12. package/.claude/skills/code-review/SKILL.md +7 -4
  13. package/.claude/skills/codebase-design/DESIGN-IT-TWICE.md +7 -4
  14. package/.claude/skills/codex-build/SKILL.md +13 -0
  15. package/.claude/skills/codex-review/SKILL.md +13 -0
  16. package/.claude/skills/grill-me-codex/SKILL.md +14 -0
  17. package/.claude/skills/grill-with-docs-codex/SKILL.md +14 -0
  18. package/.claude/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +7 -4
  19. package/.claude/skills/improve-codebase-architecture/SKILL.md +7 -4
  20. package/.claude/skills/orchestrate-wave/SKILL.md +1 -1
  21. package/.claude/skills/orchestrate-wave/references/dispatch-subagents.md +9 -0
  22. package/.claude/skills/orchestrate-wave/references/dispatch-workflow.md +9 -0
  23. package/.claude/skills/research/SKILL.md +7 -4
  24. package/.claude/skills/skill-manifest.json +34 -23
  25. package/.claude/skills/to-issues/SKILL.md +25 -4
  26. package/README.md +64 -0
  27. package/agent-workflow-kit.package.json +149 -45
  28. package/package.json +1 -1
  29. package/scripts/doctrine-migration/index.mjs +296 -0
  30. package/scripts/kit-release.mjs +41 -9
  31. package/src/cli.mjs +515 -79
  32. package/src/commands/routing-status.mjs +288 -0
  33. package/src/lib/bundle.mjs +158 -2
  34. package/src/lib/dispatchJournal.mjs +300 -0
  35. package/src/lib/dispatchPlan.mjs +286 -0
  36. package/src/lib/dispatchReceipt.mjs +226 -89
  37. package/src/lib/frontendWorkloads.mjs +35 -33
  38. package/src/lib/routeDispatcher.mjs +367 -70
  39. package/src/lib/routingAccessGraph.mjs +265 -20
  40. package/src/lib/routingAccessGraphStore.mjs +300 -0
  41. package/src/lib/routingAdapters/claude.mjs +104 -4
  42. package/src/lib/routingAdapters/codex.mjs +132 -7
  43. package/src/lib/routingAdapters/hostBridge.mjs +291 -0
  44. package/src/lib/routingCatalog.mjs +201 -24
  45. package/src/lib/routingDispatchLease.mjs +253 -0
  46. package/src/lib/routingEvidenceCache.mjs +78 -0
  47. package/src/lib/routingIntent.mjs +176 -10
  48. package/src/lib/routingIntentClassifier.mjs +137 -0
  49. package/src/lib/routingInventory/snapshots/claude.json +49 -0
  50. package/src/lib/routingInventory/snapshots/codex.json +109 -0
  51. package/src/lib/routingInventory.mjs +182 -0
  52. package/src/lib/routingPolicy.mjs +193 -6
  53. package/src/lib/routingProfile.mjs +1251 -123
  54. package/src/lib/routingProfilePolicy.mjs +156 -0
  55. package/src/lib/routingProfileStorage.mjs +299 -0
  56. package/src/lib/routingResolver.mjs +369 -86
  57. package/src/lib/routingSources/artificialAnalysis.mjs +19 -7
  58. package/src/lib/routingSources/benchlm.mjs +5 -0
  59. package/src/lib/routingSources/codeArena.mjs +13 -3
  60. package/src/lib/routingSources/deepswe.mjs +19 -5
  61. package/src/lib/routingSources/openhands.mjs +17 -4
  62. package/src/lib/routingSources/openhandsFrontend.mjs +13 -3
  63. package/src/lib/safeText.mjs +26 -0
  64. package/src/lib/updateCandidate.mjs +36 -3
@@ -1,44 +1,164 @@
1
+ /**
2
+ * The Route decision — the dispatch-time resolution of a Routing intent against
3
+ * the current Evidence catalog, Access graph and Routing policy.
4
+ *
5
+ * v2 separates provenance from execution state, because one tagged union cannot
6
+ * hold both facts: an evidence-backed candidate carries observation, source,
7
+ * harness, score, freshness and cost, which a Standard route has none of, and an
8
+ * approval-required candidate is neither executable nor blocked. The two axes
9
+ * are therefore orthogonal — `origin: evidence | standard` and `state: ready |
10
+ * approval-required | verification-required | blocked` — with a selected
11
+ * candidate whose required fields follow the origin. `status` stays the
12
+ * dispatch-facing axis and keeps the `inherit` and `handoff` fallbacks the
13
+ * Routing policy owns, which `state` deliberately cannot express.
14
+ *
15
+ * The Model roster is authorization, not evidence. An Access path whose
16
+ * model-and-effort pair the policy never authorized is refused with
17
+ * `pair-not-authorized` before any executable ranking, whatever the evidence
18
+ * says; the Evidence catalog itself is never filtered by it, so `bestOverall`
19
+ * stays the evidence view.
20
+ *
21
+ * Ranking is cohort-bound and terminates. Only observations that share an axis
22
+ * identity, a harness, an effort and a cost currency and unit are comparable;
23
+ * inside one cohort a score difference smaller than the combined uncertainty is
24
+ * no difference, so cost breaks that tie and never drives the ranking. Cohorts
25
+ * are never compared with each other: when their winners disagree the outcome is
26
+ * `ambiguous-evidence`, and the Standard route decides and says so.
27
+ */
1
28
  import {
2
29
  evidenceSelectionMatchesObservation,
3
30
  validateRoutingIntent,
4
31
  } from './routingIntent.mjs';
5
32
  import { validateEvidenceCatalog } from './routingCatalog.mjs';
6
- import { validateAccessGraph } from './routingAccessGraph.mjs';
33
+ import { accessPairKey, validateAccessGraph } from './routingAccessGraph.mjs';
7
34
  import { validateRoutingPolicy } from './routingPolicy.mjs';
35
+ import { normalizeRosterModelId } from './routingProfile.mjs';
8
36
 
9
- export const ROUTE_DECISION_VERSION = 1;
37
+ export const ROUTE_DECISION_VERSION = 2;
38
+
39
+ export const ROUTE_DECISION_ORIGINS = Object.freeze(['evidence', 'standard']);
40
+ /** What the selected candidate may do next. Pending states are not blocked. */
41
+ export const ROUTE_DECISION_STATES = Object.freeze([
42
+ 'ready', 'approval-required', 'verification-required', 'blocked',
43
+ ]);
44
+ export const BEST_OVERALL_STATES = Object.freeze(['resolved', 'ambiguous', 'unavailable']);
45
+ export const ROUTE_APPROVAL_DECISIONS = Object.freeze(['granted', 'declined']);
10
46
 
11
47
  const REQUIRED_INFRASTRUCTURE = ['catalog', 'accessGraph', 'policy'];
48
+ /** Every state that still carries a candidate, best runnable tier first. */
49
+ const CANDIDATE_TIERS = Object.freeze(
50
+ ROUTE_DECISION_STATES.filter((state) => state !== 'blocked'),
51
+ );
52
+ const APPROVAL_FIELDS = new Set(['decision', 'authorizationId']);
53
+ const NO_EVIDENCE = Object.freeze({
54
+ status: 'unavailable', route: null, cohorts: Object.freeze([]),
55
+ });
12
56
 
13
57
  function missingDecision(missing, explicitFallback) {
14
58
  return Object.freeze({
15
59
  schemaVersion: ROUTE_DECISION_VERSION,
16
60
  status: explicitFallback === 'inherit' ? 'inherit' : 'blocked',
17
61
  reason: 'routing-infrastructure-missing',
62
+ origin: null,
63
+ state: 'blocked',
18
64
  missing: Object.freeze(missing),
19
- bestOverall: null,
65
+ selected: null,
66
+ approval: null,
67
+ bestOverall: NO_EVIDENCE,
20
68
  bestExecutable: null,
21
69
  blockers: Object.freeze(missing.map((field) => `missing:${field}`)),
22
70
  revisions: Object.freeze({ catalog: null, accessGraph: null, policy: null }),
23
71
  });
24
72
  }
25
73
 
26
- function compareCandidates(optimization) {
27
- return (left, right) => {
28
- if (optimization === 'cost') {
29
- return left.cost.amount - right.cost.amount || right.score - left.score;
30
- }
31
- if (optimization === 'balanced') {
32
- const leftValue = left.score / (1 + left.cost.amount);
33
- const rightValue = right.score / (1 + right.cost.amount);
34
- return rightValue - leftValue || right.score - left.score;
35
- }
36
- return right.score - left.score || left.cost.amount - right.cost.amount;
74
+ /** A surface-switch authorization, recorded so a receipt can name it. */
75
+ function validateApproval(input) {
76
+ if (input == null) return null;
77
+ if (typeof input !== 'object' || Array.isArray(input)) {
78
+ throw new TypeError('approval must be an object');
79
+ }
80
+ for (const key of Object.keys(input)) {
81
+ if (!APPROVAL_FIELDS.has(key)) throw new TypeError(`unknown approval field: ${key}`);
82
+ }
83
+ if (!ROUTE_APPROVAL_DECISIONS.includes(input.decision)) {
84
+ throw new TypeError(`approval decision must be one of: ${ROUTE_APPROVAL_DECISIONS.join(', ')}`);
85
+ }
86
+ const authorizationId = input.authorizationId ?? null;
87
+ if (authorizationId !== null
88
+ && (typeof authorizationId !== 'string' || authorizationId.trim() === '')) {
89
+ throw new TypeError('approval authorizationId must be a non-empty string');
90
+ }
91
+ return Object.freeze({ decision: input.decision, authorizationId });
92
+ }
93
+
94
+ /** The declared cohort: axis identity, harness, effort, cost currency and unit. */
95
+ function cohortKey(observation) {
96
+ return JSON.stringify([
97
+ observation.workload,
98
+ observation.harness.id, observation.harness.version,
99
+ observation.effort,
100
+ observation.cost.currency, observation.cost.unit,
101
+ ]);
102
+ }
103
+
104
+ function compareTie(left, right) {
105
+ for (let index = 0; index < left.length; index += 1) {
106
+ const delta = typeof left[index] === 'number'
107
+ ? left[index] - right[index]
108
+ : String(left[index]).localeCompare(String(right[index]));
109
+ if (delta !== 0) return delta;
110
+ }
111
+ return 0;
112
+ }
113
+
114
+ const byScore = (left, right) =>
115
+ right.score - left.score || left.cost - right.cost || compareTie(left.tie, right.tie);
116
+ const byCost = (left, right) => left.cost - right.cost || compareTie(left.tie, right.tie);
117
+
118
+ function rankedEntry(observation, { route, tie, value }) {
119
+ return {
120
+ cohort: cohortKey(observation),
121
+ score: observation.score,
122
+ spread: Math.max(0, observation.uncertainty.value),
123
+ cost: observation.cost.amount,
124
+ route,
125
+ tie,
126
+ value,
37
127
  };
38
128
  }
39
129
 
130
+ /**
131
+ * One cohort's winner. Every candidate whose score sits inside the combined
132
+ * uncertainty of itself and the leader is indistinguishable from it, so the
133
+ * cohort's identical cost unit breaks that tie before the deterministic key.
134
+ */
135
+ function cohortWinner(entries) {
136
+ const leader = [...entries].sort(byScore)[0];
137
+ const band = entries.filter((entry) => leader.score - entry.score <= leader.spread + entry.spread);
138
+ return [...band].sort(byCost)[0];
139
+ }
140
+
141
+ /**
142
+ * Rank inside cohorts only. Several cohorts resolve when their winners nominate
143
+ * the same route; otherwise nothing dominates and the evidence is ambiguous.
144
+ */
145
+ function rankCohorts(entries) {
146
+ const cohorts = new Map();
147
+ for (const entry of entries) {
148
+ const bucket = cohorts.get(entry.cohort);
149
+ if (bucket) bucket.push(entry);
150
+ else cohorts.set(entry.cohort, [entry]);
151
+ }
152
+ const winners = [...cohorts.keys()].sort().map((key) => cohortWinner(cohorts.get(key)));
153
+ if (winners.length === 0) return { status: 'unavailable', winner: null, winners };
154
+ const routes = new Set(winners.map((entry) => entry.route));
155
+ return routes.size === 1
156
+ ? { status: 'resolved', winner: winners[0], winners }
157
+ : { status: 'ambiguous', winner: null, winners };
158
+ }
159
+
40
160
  function observationRoute(observation) {
41
- return Object.freeze({
161
+ return {
42
162
  observationId: observation.id,
43
163
  providerId: observation.providerId,
44
164
  modelId: observation.modelId,
@@ -51,52 +171,180 @@ function observationRoute(observation) {
51
171
  freshness: observation.freshness,
52
172
  cost: observation.cost,
53
173
  reason: `${observation.workload} supported by ${observation.source.id}`,
54
- });
174
+ };
55
175
  }
56
176
 
57
- function executableRoute(observation, path) {
58
- return Object.freeze({
59
- ...observationRoute(observation),
60
- accessPathId: path.id,
177
+ /** The dispatched identity always comes from the Access path, never the evidence. */
178
+ function pathIdentity(path) {
179
+ return {
180
+ providerId: path.providerId,
181
+ modelId: path.modelId,
182
+ effort: path.effort,
61
183
  surfaceId: path.surfaceId,
62
184
  transportId: path.transportId,
185
+ accessPathId: path.id,
63
186
  enforcement: path.enforcement,
64
187
  capabilityEvidence: path.capabilityEvidence,
188
+ };
189
+ }
190
+
191
+ const NO_PATH_IDENTITY = Object.freeze({
192
+ providerId: null, surfaceId: null, transportId: null,
193
+ accessPathId: null, enforcement: null, capabilityEvidence: null,
194
+ });
195
+
196
+ function evidenceCandidate({ observation, path, state }) {
197
+ return Object.freeze({
198
+ origin: 'evidence',
199
+ state,
200
+ ...observationRoute(observation),
201
+ ...pathIdentity(path),
202
+ });
203
+ }
204
+
205
+ function standardCandidate(route, workloadClass, path, state) {
206
+ return Object.freeze({
207
+ origin: 'standard',
208
+ state,
209
+ workloadClass,
210
+ ...NO_PATH_IDENTITY,
211
+ modelId: route.model,
212
+ effort: route.effort,
213
+ ...(path ? pathIdentity(path) : {}),
214
+ reason: `standard-route:${workloadClass}`,
65
215
  });
66
216
  }
67
217
 
68
- function pathAllowed(path, policy, activeSurface, knownTransports, now, blockers) {
218
+ const pairLabel = (path) => `${normalizeRosterModelId(path.modelId)}+${path.effort ?? 'none'}`;
219
+ const rosterKey = ({ model, effort }) => JSON.stringify([model, effort ?? null]);
220
+
221
+ function rosterAuthorizes(policy, path) {
222
+ const key = rosterKey({ model: normalizeRosterModelId(path.modelId), effort: path.effort });
223
+ return policy.roster.some((pair) => rosterKey(pair) === key);
224
+ }
225
+
226
+ const denied = (blocker) => ({ state: 'blocked', blocker });
227
+
228
+ function refusedPath(path, { policy, knownTransports, now }) {
69
229
  if (!knownTransports.has(path.transportId)) {
70
- blockers.add(`unknown-transport:${path.transportId}`);
71
- return false;
230
+ return denied(`unknown-transport:${path.transportId}`);
72
231
  }
73
232
  if (Date.parse(path.capabilityEvidence.expiresAt) <= now) {
74
- blockers.add(`stale-capability-evidence:${path.id}`);
75
- return false;
76
- }
77
- if (path.availability !== 'available') {
78
- blockers.add(`route-${path.availability}:${path.id}`);
79
- return false;
233
+ return denied(`stale-capability-evidence:${path.id}`);
80
234
  }
81
235
  if (!policy.allowedSurfaces.includes(path.surfaceId)) {
82
- blockers.add(`surface-not-allowed:${path.surfaceId}`);
83
- return false;
236
+ return denied(`surface-not-allowed:${path.surfaceId}`);
84
237
  }
85
238
  if (!policy.allowedTransports.includes(path.transportId)) {
86
- blockers.add(`transport-not-allowed:${path.transportId}`);
87
- return false;
239
+ return denied(`transport-not-allowed:${path.transportId}`);
88
240
  }
89
- if (path.surfaceId !== activeSurface) {
90
- if (policy.switching === 'current-surface-only') {
91
- blockers.add(`surface-switch-disabled:${path.surfaceId}`);
92
- return false;
93
- }
94
- if (policy.switching === 'ask') {
95
- blockers.add(`surface-switch-approval-required:${path.surfaceId}`);
96
- return false;
241
+ // The roster is a positive list: an unauthorized pair never reaches a ranking.
242
+ if (!rosterAuthorizes(policy, path)) return denied(`pair-not-authorized:${pairLabel(path)}`);
243
+ if (path.availability === 'unavailable') return denied(`route-unavailable:${path.id}`);
244
+ return null;
245
+ }
246
+
247
+ /** Untested access verifies under supervision and stays blocked for an AFK run. */
248
+ function attestationState(path, { afk }) {
249
+ if (path.availability === 'available') return null;
250
+ if (afk) return denied(`afk-requires-attested-access:${path.id}`);
251
+ return { state: 'verification-required', blocker: `access-unknown:${path.id}` };
252
+ }
253
+
254
+ /** Cross-surface autonomy: `ask` carries the candidate into an approval decision. */
255
+ function switchState(path, { policy, activeSurface, approval }) {
256
+ if (path.surfaceId === activeSurface || policy.switching === 'automatic') return null;
257
+ if (policy.switching === 'current-surface-only') {
258
+ return denied(`surface-switch-disabled:${path.surfaceId}`);
259
+ }
260
+ if (approval?.decision === 'declined') return denied(`approval-declined:${path.surfaceId}`);
261
+ if (approval?.decision === 'granted') return null;
262
+ return {
263
+ state: 'approval-required',
264
+ blocker: `surface-switch-approval-required:${path.surfaceId}`,
265
+ };
266
+ }
267
+
268
+ /**
269
+ * One path's executable state. A refusal always wins; when both a surface
270
+ * approval and an attestation are pending the approval is reported, because
271
+ * approving an attested route costs less than probing an unattested one.
272
+ */
273
+ function classifyPath(path, context) {
274
+ const refused = refusedPath(path, context);
275
+ if (refused) return refused;
276
+ const attested = attestationState(path, context);
277
+ if (attested?.state === 'blocked') return attested;
278
+ const approval = switchState(path, context);
279
+ if (approval?.state === 'blocked') return approval;
280
+ return approval ?? attested ?? { state: 'ready', blocker: null };
281
+ }
282
+
283
+ function classifiedPath(path, context) {
284
+ const classified = classifyPath(path, context);
285
+ if (classified.blocker) context.blockers.add(classified.blocker);
286
+ return classified;
287
+ }
288
+
289
+ const surfaceTie = (path, activeSurface) => [path.surfaceId === activeSurface ? 0 : 1, path.id];
290
+
291
+ /**
292
+ * The candidates one observation and one Access path form. An observation ranks
293
+ * a model, so it pairs with every path to that model; which effort is dispatched
294
+ * stays the path's own attested pair, and the roster authorizes that pair.
295
+ */
296
+ function candidateEntries(evidence, paths, context) {
297
+ const entries = [];
298
+ for (const observation of evidence) {
299
+ for (const path of paths) {
300
+ if (path.providerId !== observation.providerId || path.modelId !== observation.modelId) {
301
+ continue;
302
+ }
303
+ const { state } = classifiedPath(path, context);
304
+ if (state === 'blocked') continue;
305
+ entries.push(rankedEntry(observation, {
306
+ route: accessPairKey(path),
307
+ tie: [...surfaceTie(path, context.activeSurface), observation.id],
308
+ value: { observation, path, state },
309
+ }));
97
310
  }
98
311
  }
99
- return true;
312
+ return entries;
313
+ }
314
+
315
+ /** The best runnable tier decides; a pending candidate never outranks a ready one. */
316
+ function tieredRanking(entries) {
317
+ for (const state of CANDIDATE_TIERS) {
318
+ const tier = entries.filter((entry) => entry.value.state === state);
319
+ if (tier.length > 0) return rankCohorts(tier);
320
+ }
321
+ return rankCohorts([]);
322
+ }
323
+
324
+ /**
325
+ * The Standard route for the resolved workload class. It authorizes nothing
326
+ * while unresolved, and it is named even when no path can currently take it.
327
+ */
328
+ function standardSelection(workloadClass, paths, context) {
329
+ const route = context.policy.standardRoutes[workloadClass];
330
+ if (!route || route.state !== 'configured') {
331
+ context.blockers.add(`standard-route-unresolved:${workloadClass}`);
332
+ return null;
333
+ }
334
+ const candidates = paths
335
+ .filter((path) => normalizeRosterModelId(path.modelId) === route.model
336
+ && (path.effort ?? null) === (route.effort ?? null))
337
+ .map((path) => ({ path, ...classifiedPath(path, context) }))
338
+ .sort((left, right) => compareTie(
339
+ surfaceTie(left.path, context.activeSurface),
340
+ surfaceTie(right.path, context.activeSurface),
341
+ ));
342
+ for (const state of CANDIDATE_TIERS) {
343
+ const usable = candidates.find((entry) => entry.state === state);
344
+ if (usable) return standardCandidate(route, workloadClass, usable.path, state);
345
+ }
346
+ context.blockers.add(`standard-route-unreachable:${route.model}+${route.effort ?? 'none'}`);
347
+ return standardCandidate(route, workloadClass, null, 'blocked');
100
348
  }
101
349
 
102
350
  function noExecutableStatus(policy) {
@@ -105,29 +353,21 @@ function noExecutableStatus(policy) {
105
353
  return 'blocked';
106
354
  }
107
355
 
108
- export function resolveRoute(input) {
109
- if (!input || typeof input !== 'object' || Array.isArray(input)) {
110
- throw new TypeError('route resolution input must be an object');
111
- }
112
- const missing = REQUIRED_INFRASTRUCTURE.filter((field) => input[field] == null);
113
- if (missing.length > 0) return missingDecision(missing, input.missingInfrastructure);
356
+ /** A pending decision is owed to a human; it never inherits or hands off silently. */
357
+ function decisionStatus(state, policy) {
358
+ if (state === 'ready') return 'ready';
359
+ if (state === 'blocked') return noExecutableStatus(policy);
360
+ return 'blocked';
361
+ }
114
362
 
115
- const intent = validateRoutingIntent(input.intent);
116
- const catalog = validateEvidenceCatalog(input.catalog);
117
- const accessGraph = validateAccessGraph(input.accessGraph);
118
- const policy = validateRoutingPolicy(input.policy);
119
- if (typeof input.activeSurface !== 'string' || input.activeSurface === '') {
120
- throw new TypeError('activeSurface must be a non-empty string');
121
- }
122
- if (!Array.isArray(input.knownTransports)
123
- || !input.knownTransports.every((entry) => typeof entry === 'string' && entry !== '')) {
124
- throw new TypeError('knownTransports must be an array of non-empty strings');
125
- }
126
- const now = Date.parse(input.now);
127
- if (!Number.isFinite(now)) throw new TypeError('now must be an ISO timestamp');
363
+ function decisionReason(selected, fallbackReason) {
364
+ if (!selected) return 'no-executable-route';
365
+ if (selected.origin === 'standard') return fallbackReason;
366
+ return selected.state === 'ready' ? 'route-resolved' : selected.state;
367
+ }
128
368
 
129
- const blockers = new Set();
130
- const currentEvidence = catalog.observations.filter((entry) => {
369
+ function currentEvidenceFor(intent, catalog, now, blockers) {
370
+ return catalog.observations.filter((entry) => {
131
371
  const matchesIntent = intent.evidenceSelection
132
372
  ? evidenceSelectionMatchesObservation(intent.evidenceSelection, entry.workload)
133
373
  : entry.workload === intent.workload;
@@ -138,34 +378,77 @@ export function resolveRoute(input) {
138
378
  }
139
379
  return true;
140
380
  });
141
- const rankedOverall = [...currentEvidence].sort(compareCandidates(policy.optimization));
142
- const bestOverall = rankedOverall.length > 0 ? observationRoute(rankedOverall[0]) : null;
143
-
144
- const knownTransports = new Set(input.knownTransports);
145
- const executable = [];
146
- for (const evidence of currentEvidence) {
147
- for (const path of accessGraph.paths) {
148
- if (path.providerId !== evidence.providerId || path.modelId !== evidence.modelId) continue;
149
- if (!pathAllowed(path, policy, input.activeSurface, knownTransports, now, blockers)) continue;
150
- executable.push({ evidence, path });
151
- }
381
+ }
382
+
383
+ function evidenceView(evidence) {
384
+ const ranking = rankCohorts(evidence.map((observation) => rankedEntry(observation, {
385
+ route: JSON.stringify([observation.providerId, observation.modelId, observation.effort]),
386
+ tie: [observation.id],
387
+ value: observation,
388
+ })));
389
+ return Object.freeze({
390
+ status: ranking.status,
391
+ route: ranking.winner ? Object.freeze(observationRoute(ranking.winner.value)) : null,
392
+ cohorts: Object.freeze(ranking.winners.map((entry) =>
393
+ Object.freeze(observationRoute(entry.value)))),
394
+ });
395
+ }
396
+
397
+ function validatedContext(input, { policy, intent, blockers }) {
398
+ if (typeof input.activeSurface !== 'string' || input.activeSurface === '') {
399
+ throw new TypeError('activeSurface must be a non-empty string');
400
+ }
401
+ if (!Array.isArray(input.knownTransports)
402
+ || !input.knownTransports.every((entry) => typeof entry === 'string' && entry !== '')) {
403
+ throw new TypeError('knownTransports must be an array of non-empty strings');
404
+ }
405
+ const now = Date.parse(input.now);
406
+ if (!Number.isFinite(now)) throw new TypeError('now must be an ISO timestamp');
407
+ return {
408
+ policy,
409
+ activeSurface: input.activeSurface,
410
+ knownTransports: new Set(input.knownTransports),
411
+ now,
412
+ afk: intent.autonomyRequirement === 'afk',
413
+ approval: validateApproval(input.approval),
414
+ blockers,
415
+ };
416
+ }
417
+
418
+ export function resolveRoute(input) {
419
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
420
+ throw new TypeError('route resolution input must be an object');
152
421
  }
153
- executable.sort((left, right) =>
154
- compareCandidates(policy.optimization)(left.evidence, right.evidence)
155
- || Number(right.path.surfaceId === input.activeSurface)
156
- - Number(left.path.surfaceId === input.activeSurface)
157
- || left.path.id.localeCompare(right.path.id));
158
- const bestExecutable = executable.length > 0
159
- ? executableRoute(executable[0].evidence, executable[0].path)
160
- : null;
161
- const status = bestExecutable ? 'ready' : noExecutableStatus(policy);
422
+ const missing = REQUIRED_INFRASTRUCTURE.filter((field) => input[field] == null);
423
+ if (missing.length > 0) return missingDecision(missing, input.missingInfrastructure);
424
+
425
+ const intent = validateRoutingIntent(input.intent);
426
+ const catalog = validateEvidenceCatalog(input.catalog);
427
+ const accessGraph = validateAccessGraph(input.accessGraph);
428
+ const policy = validateRoutingPolicy(input.policy);
429
+ const blockers = new Set();
430
+ const context = validatedContext(input, { policy, intent, blockers });
431
+
432
+ const evidence = currentEvidenceFor(intent, catalog, context.now, blockers);
433
+ const bestOverall = evidenceView(evidence);
434
+ const ranking = tieredRanking(candidateEntries(evidence, accessGraph.paths, context));
435
+ const fallbackReason = ranking.status === 'ambiguous' ? 'ambiguous-evidence' : 'no-evidence-route';
436
+ const selected = ranking.winner
437
+ ? evidenceCandidate(ranking.winner.value)
438
+ : standardSelection(intent.workload, accessGraph.paths, context);
439
+ const state = selected?.state ?? 'blocked';
440
+
162
441
  return Object.freeze({
163
442
  schemaVersion: ROUTE_DECISION_VERSION,
164
- status,
165
- reason: bestExecutable ? 'route-resolved' : 'no-executable-route',
443
+ status: decisionStatus(state, policy),
444
+ reason: decisionReason(selected, fallbackReason),
445
+ origin: selected?.origin ?? null,
446
+ state,
166
447
  intent,
448
+ selected,
449
+ approval: context.approval,
167
450
  bestOverall,
168
- bestExecutable,
451
+ bestExecutable: state === 'ready' ? selected : null,
169
452
  blockers: Object.freeze([...blockers].sort()),
170
453
  revisions: Object.freeze({
171
454
  catalog: catalog.revision,
@@ -1,6 +1,14 @@
1
+ import {
2
+ assertPublishedEffort,
3
+ evidenceFreshness,
4
+ evidenceIdentity,
5
+ evidenceSourceClaim,
6
+ } from '../routingCatalog.mjs';
7
+
1
8
  const SOURCE_ID = 'artificial-analysis-coding-agents';
2
9
  const OWNER = 'Artificial Analysis';
3
10
  const ARTIFACT_URL = 'https://artificialanalysis.ai/agents/coding-agents';
11
+ const WORKLOAD = evidenceIdentity({ workload: 'repository-repair', axis: 'functional' });
4
12
 
5
13
  function object(value, field) {
6
14
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
@@ -60,10 +68,13 @@ function ingest({ payload, snapshotHash, observedAt, expiresAt }) {
60
68
  row.modelId,
61
69
  `Artificial Analysis configurations[${index}].modelId`,
62
70
  );
63
- const effort = string(
64
- row.reasoningEffort,
65
- `Artificial Analysis configurations[${index}].effort`,
66
- );
71
+ const effort = assertPublishedEffort({
72
+ sourceId: SOURCE_ID,
73
+ effort: string(
74
+ row.reasoningEffort,
75
+ `Artificial Analysis configurations[${index}].effort`,
76
+ ),
77
+ });
67
78
  const id = `${SOURCE_ID}:${benchmarkVersion}:${providerId}:${modelId}:${effort}`;
68
79
  if (observationIds.has(id)) {
69
80
  throw new TypeError(`duplicate Artificial Analysis observation: ${id}`);
@@ -79,7 +90,7 @@ function ingest({ payload, snapshotHash, observedAt, expiresAt }) {
79
90
  providerId,
80
91
  modelId,
81
92
  effort,
82
- workload: 'development',
93
+ workload: WORKLOAD,
83
94
  harness: { id: harnessId, version: harnessVersion },
84
95
  score: number(
85
96
  row.indexScore,
@@ -103,14 +114,14 @@ function ingest({ payload, snapshotHash, observedAt, expiresAt }) {
103
114
  `Artificial Analysis configurations[${index}].uncertainty.value`,
104
115
  ),
105
116
  },
106
- freshness: { observedAt, expiresAt },
117
+ freshness: evidenceFreshness({ sourceId: SOURCE_ID, observedAt }),
107
118
  cost: {
108
119
  amount: number(
109
120
  row.costPerTaskUsd,
110
121
  `Artificial Analysis configurations[${index}].costPerTaskUsd`,
111
122
  ),
112
123
  currency: 'USD',
113
- unit: 'task',
124
+ unit: 'attempt',
114
125
  },
115
126
  });
116
127
  });
@@ -125,5 +136,6 @@ export const artificialAnalysisSource = Object.freeze({
125
136
  sourceId: SOURCE_ID,
126
137
  owner: OWNER,
127
138
  artifactUrl: ARTIFACT_URL,
139
+ claim: evidenceSourceClaim(SOURCE_ID),
128
140
  ingest,
129
141
  });
@@ -1,3 +1,5 @@
1
+ import { evidenceFreshness, evidenceSourceClaim } from '../routingCatalog.mjs';
2
+
1
3
  const SOURCE_ID = 'benchlm';
2
4
  const ARTIFACT_URL = 'https://benchlm.ai/data';
3
5
  const OWNER_URLS = Object.freeze({
@@ -128,6 +130,8 @@ function ingest({ payload, snapshotHash, observedAt, expiresAt }) {
128
130
  generatedAt,
129
131
  sourceLastUpdated,
130
132
  snapshotHash,
133
+ // The owner publishes no cadence, so the Kit dates its own expiry.
134
+ ...evidenceFreshness({ sourceId: SOURCE_ID, observedAt }),
131
135
  });
132
136
  signals.push({
133
137
  kind: 'corroboration-candidate',
@@ -147,5 +151,6 @@ export const benchLmSource = Object.freeze({
147
151
  sourceId: SOURCE_ID,
148
152
  owner: 'BenchLM',
149
153
  artifactUrl: ARTIFACT_URL,
154
+ claim: evidenceSourceClaim(SOURCE_ID),
150
155
  ingest,
151
156
  });
@@ -1,4 +1,10 @@
1
1
  import { frontendEvidenceWorkload } from '../frontendWorkloads.mjs';
2
+ import {
3
+ assertCostUnit,
4
+ assertPublishedEffort,
5
+ evidenceFreshness,
6
+ evidenceSourceClaim,
7
+ } from '../routingCatalog.mjs';
2
8
 
3
9
  const OWNER_URL = 'https://arena.ai/leaderboard/code/webdev';
4
10
  const SOURCE_ID = 'code-arena-webdev';
@@ -80,7 +86,10 @@ function ingest({ payload, snapshotHash, observedAt, expiresAt }) {
80
86
  object(row, `Code Arena rows[${index}]`);
81
87
  const providerId = string(row.providerId, `Code Arena rows[${index}].providerId`);
82
88
  const modelId = string(row.modelId, `Code Arena rows[${index}].modelId`);
83
- const effort = string(row.effort, `Code Arena rows[${index}].effort`);
89
+ const effort = assertPublishedEffort({
90
+ sourceId: SOURCE_ID,
91
+ effort: string(row.effort, `Code Arena rows[${index}].effort`),
92
+ });
84
93
  const domain = string(row.domain, `Code Arena rows[${index}].domain`);
85
94
  const score = finite(row.score, `Code Arena rows[${index}].score`);
86
95
  const interval = finite(
@@ -132,11 +141,11 @@ function ingest({ payload, snapshotHash, observedAt, expiresAt }) {
132
141
  status: row.status,
133
142
  sampleSize,
134
143
  },
135
- freshness: { observedAt, expiresAt },
144
+ freshness: evidenceFreshness({ sourceId: SOURCE_ID, observedAt }),
136
145
  cost: {
137
146
  amount: finite(row.cost.amount, `Code Arena rows[${index}].cost.amount`),
138
147
  currency: string(row.cost.currency, `Code Arena rows[${index}].cost.currency`),
139
- unit: string(row.cost.unit, `Code Arena rows[${index}].cost.unit`),
148
+ unit: assertCostUnit(row.cost.unit, `Code Arena rows[${index}].cost.unit`),
140
149
  },
141
150
  });
142
151
  }
@@ -158,5 +167,6 @@ export const codeArenaSource = Object.freeze({
158
167
  sourceId: SOURCE_ID,
159
168
  owner: 'Arena',
160
169
  artifactUrl: OWNER_URL,
170
+ claim: evidenceSourceClaim(SOURCE_ID),
161
171
  ingest,
162
172
  });