@kal-elsam/kairo-runtime 0.15.0 → 0.17.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 (92) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/package.json +2 -1
  3. package/scripts/cockpit-smoke.mjs +1 -1
  4. package/scripts/ux-smoke-test.sh +3 -3
  5. package/src/cli.js +106 -11
  6. package/src/global/agent-capabilities/create-capability-adapter.js +2 -2
  7. package/src/global/architect/architect-cli.js +76 -0
  8. package/src/global/architect/architect-codex.js +146 -0
  9. package/src/global/architect/architect-manager.js +125 -0
  10. package/src/global/architect/architect-store.js +377 -0
  11. package/src/global/architect/architect-types.js +47 -0
  12. package/src/global/cli-help.js +12 -1
  13. package/src/global/cockpit/app.js +475 -0
  14. package/src/global/cockpit/card.js +111 -0
  15. package/src/global/cockpit/cli.js +33 -0
  16. package/src/global/cockpit/gauge.js +31 -0
  17. package/src/global/cockpit/project-overlay.js +683 -0
  18. package/src/global/cockpit/rows.js +148 -0
  19. package/src/global/cockpit/theme.js +118 -0
  20. package/src/global/cockpit/view.js +1263 -0
  21. package/src/global/control-plane/attention.js +141 -0
  22. package/src/global/control-plane/build-report.js +146 -0
  23. package/src/global/control-plane/cli.js +36 -0
  24. package/src/global/control-plane/constants.js +38 -0
  25. package/src/global/control-plane/gentle-adapters.js +183 -0
  26. package/src/global/control-plane/provider.js +69 -0
  27. package/src/global/control-plane/review-status.js +115 -0
  28. package/src/global/control-plane/sdd-status.js +49 -0
  29. package/src/global/control-plane/team.js +63 -0
  30. package/src/global/conversation/bootstrap-analyzer-adapters.js +251 -0
  31. package/src/global/conversation/cli.js +53 -0
  32. package/src/global/conversation/codex-sandbox.js +230 -0
  33. package/src/global/conversation/cursor-sandbox.js +215 -0
  34. package/src/global/conversation/project-analysis.js +204 -0
  35. package/src/global/conversation/project-profile.js +178 -0
  36. package/src/global/conversation/project-router.js +149 -0
  37. package/src/global/conversation/project-strategy-store.js +64 -0
  38. package/src/global/conversation/project-strategy.js +514 -0
  39. package/src/global/conversation/sanitized-snapshot.js +169 -0
  40. package/src/global/conversation/secret-scanner.js +71 -0
  41. package/src/global/conversation/service.js +1063 -0
  42. package/src/global/conversation/session-store.js +75 -0
  43. package/src/global/conversation/transcript-store.js +79 -0
  44. package/src/global/conversation/ui.js +195 -0
  45. package/src/global/intelligence/capability-scoring.js +480 -0
  46. package/src/global/intelligence/execution-router.js +444 -0
  47. package/src/global/intelligence/kairo-telemetry-source.js +59 -0
  48. package/src/global/intelligence/kairobench-runner.js +85 -0
  49. package/src/global/intelligence/kairobench-source.js +34 -0
  50. package/src/global/intelligence/kairobench-tasks.js +47 -0
  51. package/src/global/intelligence/model-candidate-catalog.js +456 -0
  52. package/src/global/intelligence/model-capability-registry-sources.js +145 -0
  53. package/src/global/intelligence/model-capability-registry.js +125 -0
  54. package/src/global/intelligence/model-intelligence.js +1646 -0
  55. package/src/global/intelligence/official-benchmark-snapshots.js +162 -0
  56. package/src/global/intelligence/quick-ask.js +149 -0
  57. package/src/global/intelligence/role-profiles.js +251 -0
  58. package/src/global/intelligence/skill-catalog.js +67 -0
  59. package/src/global/intelligence/subscription-pressure-source.js +41 -0
  60. package/src/global/mcp/kairo-mcp.js +51 -18
  61. package/src/global/mcp/work-snapshot-rule.js +4 -2
  62. package/src/global/mcp/workspace-binding.js +88 -0
  63. package/src/global/mcp/workspace-mcp-entry.js +74 -0
  64. package/src/global/mcp-install.js +8 -1
  65. package/src/global/observability/artificial-analysis-models.js +118 -0
  66. package/src/global/observability/claude-models.js +31 -0
  67. package/src/global/observability/claude-usage.js +112 -0
  68. package/src/global/observability/codex-models.js +96 -0
  69. package/src/global/observability/codex-usage.js +160 -0
  70. package/src/global/observability/cursor-auth.js +88 -0
  71. package/src/global/observability/cursor-models.js +101 -0
  72. package/src/global/observability/gentle-probe.js +30 -2
  73. package/src/global/observability/huggingface-leaderboard.js +97 -0
  74. package/src/global/observability/index.js +2 -1
  75. package/src/global/observability/opencode-models.js +101 -0
  76. package/src/global/observability/opencode-usage.js +162 -0
  77. package/src/global/paths.js +49 -2
  78. package/src/global/profile.js +23 -1
  79. package/src/global/runtime/execution-adapters/claude.js +63 -30
  80. package/src/global/runtime/execution-adapters/codex.js +9 -2
  81. package/src/global/runtime/execution-adapters/create-execution-adapter.js +6 -1
  82. package/src/global/runtime/execution-adapters/opencode.js +83 -18
  83. package/src/global/runtime/execution-worktree-manager.js +924 -0
  84. package/src/global/runtime/execution-worktree-orchestrator.js +194 -0
  85. package/src/global/runtime/execution-worktree-store.js +83 -0
  86. package/src/global/runtime/execution-worktree-types.js +45 -0
  87. package/src/global/runtime/run-events.js +38 -0
  88. package/src/global/runtime/run-manager.js +22 -6
  89. package/src/global/runtime/run-supervisor.js +41 -12
  90. package/src/global/runtime/usage-manager.js +96 -0
  91. package/src/global/runtime/usage-store.js +69 -0
  92. package/src/global/runtime/usage-types.js +62 -0
@@ -0,0 +1,480 @@
1
+ // Robust multi-metric role scoring: percentile-normalized, confidence-aware,
2
+ // never a fabricated zero for missing evidence. Consumes the same raw
3
+ // CapabilityRegistry evidence (model-capability-registry.js) that AI TEAM
4
+ // always has — this module owns turning that evidence into a per-role
5
+ // score; it never collects evidence itself.
6
+ //
7
+ // Why percentiles, not the AA composite indices directly: AA's
8
+ // intelligenceIndex/codingIndex are already a blend Kairo doesn't control
9
+ // (which benchmarks, what weights) and mix 0-100/0-1 scales across
10
+ // sources. Ranking on percentile position within Kairo's own real,
11
+ // accessible candidate pool sidesteps the whole scale question — relative
12
+ // order is scale-invariant — and stops one blended vendor number from
13
+ // silently deciding a role Kairo could score on its own real component
14
+ // benchmarks instead.
15
+ //
16
+ // Percentile alone can't decide EVERYTHING, though: with Kairo's typical
17
+ // real candidate pool (2-3 accessible providers), a percentile rank is
18
+ // always exactly {0, 0.5, 1} — it encodes ORDER, never real magnitude, by
19
+ // construction. Two models 1% apart and two models 40% apart both just
20
+ // read "1 vs 0" with only two real candidates. So this module exports two
21
+ // parallel, deliberately different numbers per model: capabilityPercentile
22
+ // (robust rank position — decides WHO'S AHEAD) and a separate real,
23
+ // scale-normalized gap value (decides HOW CLOSE — near-equivalence bands
24
+ // and capability floors in model-intelligence.js compare THIS, never the
25
+ // percentile, so small pools keep real granularity instead of collapsing
26
+ // every non-winner to a floor-failing 0).
27
+
28
+ /**
29
+ * @typedef {"reasoning"|"coding"|"terminalExecution"|"softwareExecution"|"instructionFollowing"} Capability
30
+ */
31
+
32
+ /**
33
+ * One real, distinct benchmark identity per row — `metricAliases` lists
34
+ * every metric NAME any connected source uses for that SAME real
35
+ * benchmark (e.g. AA's free API reports GPQA as "gpqa"; a manufacturer's
36
+ * own launch-page table reports the identical benchmark as
37
+ * "gpqa-diamond"). Deduplicated via bestEvidence-style pick (verified
38
+ * first, then most recent) BEFORE ranking, so the same real benchmark
39
+ * reported by two sources counts once, never twice in a capability's
40
+ * median. Distinct real benchmarks (GPQA vs HLE — Humanity's Last Exam is
41
+ * a different exam, not a rename) always stay separate rows.
42
+ *
43
+ * terminalBenchV2/terminalBenchHard/terminal-bench/terminal-bench-science
44
+ * are pooled as one identity here, inheriting the same grouping this
45
+ * codebase's CANONICAL_CAPABILITIES table already uses elsewhere — not a
46
+ * new decision introduced by this module.
47
+ *
48
+ * tauBanking is deliberately excluded from softwareExecution: it's kept
49
+ * as a separate agentic/tool-use signal, never counted as software
50
+ * engineering capability, per explicit product decision.
51
+ * Each alias also carries its own real `scale` ("unit": 0-1 fraction, as
52
+ * AA's free API and most independent evals report; "hundred": 0-100
53
+ * score, as manufacturer launch tables tend to report) — used ONLY by the
54
+ * real-gap calculation below, never by the percentile ranking (which is
55
+ * scale-invariant by construction).
56
+ * @type {Array<{benchmarkId: string, capability: Capability, metricAliases: Array<{metric: string, scale: "unit"|"hundred"}>}>}
57
+ */
58
+ export const BENCHMARK_IDENTITIES = [
59
+ { benchmarkId: "gpqa", capability: "reasoning", metricAliases: [{ metric: "gpqa", scale: "unit" }, { metric: "gpqa-diamond", scale: "hundred" }] },
60
+ { benchmarkId: "hle", capability: "reasoning", metricAliases: [{ metric: "hle", scale: "unit" }] },
61
+ { benchmarkId: "mmlu-pro", capability: "reasoning", metricAliases: [{ metric: "mmluPro", scale: "unit" }] },
62
+ { benchmarkId: "livecodebench", capability: "coding", metricAliases: [{ metric: "liveCodeBench", scale: "unit" }] },
63
+ { benchmarkId: "scicode", capability: "coding", metricAliases: [{ metric: "sciCode", scale: "unit" }] },
64
+ { benchmarkId: "terminal-bench", capability: "terminalExecution", metricAliases: [
65
+ { metric: "terminalBenchV2", scale: "unit" }, { metric: "terminalBenchHard", scale: "unit" },
66
+ { metric: "terminal-bench", scale: "hundred" }, { metric: "terminal-bench-science", scale: "hundred" }
67
+ ] },
68
+ { benchmarkId: "cursorbench", capability: "softwareExecution", metricAliases: [{ metric: "cursorbench", scale: "hundred" }] },
69
+ { benchmarkId: "kairo-success", capability: "softwareExecution", metricAliases: [{ metric: "kairo.success", scale: "unit" }] },
70
+ { benchmarkId: "ifbench", capability: "instructionFollowing", metricAliases: [{ metric: "ifBench", scale: "unit" }] }
71
+ ];
72
+
73
+ // A capability falls back to AA's own composite index ONLY when none of
74
+ // its component benchmarks (above) produced a real, comparable cohort
75
+ // anywhere in the current candidate pool — never alongside real component
76
+ // benchmarks (that would double-count the same underlying capability:
77
+ // once via its real components, once via the blended index that already
78
+ // includes them).
79
+ export const COMPOSITE_FALLBACKS = { reasoning: "intelligenceIndex", coding: "codingIndex" };
80
+
81
+ function activeBenchmarkIdentities(capability) {
82
+ return BENCHMARK_IDENTITIES.filter((identity) => identity.capability === capability);
83
+ }
84
+
85
+ /** How many of a capability's real, distinct benchmark identities exist at all — reasoning: 3 (gpqa/hle/mmlu-pro), coding: 2, terminalExecution/instructionFollowing: 1 each. Static reference data (never per-model) — used by /models --evidence to render "X/Y benchmarks" alongside a real model's own count. */
86
+ export function activeBenchmarkCountForCapability(capability) {
87
+ return activeBenchmarkIdentities(capability).length;
88
+ }
89
+
90
+ /**
91
+ * How many of this capability's real, distinct benchmark identities this
92
+ * EXACT model has real evidence for — counting identities (AA's "hle" and
93
+ * Hugging Face's "hle" are the SAME real benchmark, counted once), never
94
+ * raw evidence entries or sources. Deliberately independent of whether a
95
+ * comparable cohort formed for ranking (percentileForBenchmark's own,
96
+ * separate concern) — this measures how much of the capability THIS
97
+ * candidate's own evidence actually covers, regardless of who else is in
98
+ * the pool.
99
+ * @param {ReturnType<import("./model-capability-registry.js").createCapabilityRegistry>} registry
100
+ * @param {object} model
101
+ * @param {Capability} capability
102
+ * @returns {number}
103
+ */
104
+ export function countModelBenchmarks(registry, model, capability) {
105
+ const id = registry.registerIdentity(model.adapterId, model.modelId, model.displayName ?? null);
106
+ let count = 0;
107
+ for (const identity of activeBenchmarkIdentities(capability)) {
108
+ if (bestAcrossAliases(registry, id, identity)) count += 1;
109
+ }
110
+ return count;
111
+ }
112
+
113
+ // A capability is "comparable" once real, distinct benchmark coverage
114
+ // reaches at least half of its real active benchmarks, rounded UP —
115
+ // reasoning (3 active) needs 2, coding (2 active) needs 1, and a
116
+ // capability with only ever one real known benchmark (terminalExecution,
117
+ // instructionFollowing) needs just that one — the 50% floor never
118
+ // demands evidence the real catalog structurally can't provide. Below
119
+ // that floor, a candidate is "provisional" for the capability: it can
120
+ // still be scored and ranked (never excluded outright — a real single
121
+ // data point is still real evidence), but never wins outright over a
122
+ // real, more broadly comparable candidate — see model-intelligence.js's
123
+ // own comparable-before-provisional selection order.
124
+ const COMPARABILITY_THRESHOLD_RATIO = 0.5;
125
+
126
+ /**
127
+ * @param {Capability} capability
128
+ * @param {number} benchmarkCount - this model's own real distinct benchmark count for the capability (see countModelBenchmarks)
129
+ * @returns {boolean}
130
+ */
131
+ export function isCapabilityComparable(capability, benchmarkCount) {
132
+ const active = activeBenchmarkCountForCapability(capability);
133
+ if (active === 0) return true; // no known real benchmark for this capability at all — never gate on something unmeasurable
134
+ return benchmarkCount >= Math.ceil(active * COMPARABILITY_THRESHOLD_RATIO);
135
+ }
136
+
137
+ // Metrics where a LOWER value is the better real result. Every metric in
138
+ // BENCHMARK_IDENTITIES today is higher-is-better; this stays a real,
139
+ // checked table (not a hardcoded assumption baked into the ranking math)
140
+ // so a future lower-is-better benchmark is handled correctly without
141
+ // silently ranking it backwards.
142
+ const LOWER_IS_BETTER_METRICS = new Set();
143
+
144
+ function modelKey(model) {
145
+ return `${model.adapterId}::${model.modelId}`;
146
+ }
147
+
148
+ function directionFor(metric) {
149
+ return LOWER_IS_BETTER_METRICS.has(metric) ? "lower" : "higher";
150
+ }
151
+
152
+ function convertByScale(scale, value) {
153
+ return scale === "hundred" ? value / 100 : value;
154
+ }
155
+
156
+ /**
157
+ * The single most trustworthy real entry for a model across every metric
158
+ * alias of one benchmark identity — verified first, then most recent,
159
+ * exactly bestEvidence()'s own rule, just applied across aliases instead
160
+ * of a single metric name. This is the dedup step: the same real
161
+ * benchmark reported under two different source-specific key names never
162
+ * produces two competing data points for the same model. Returns the
163
+ * alias's own `scale` alongside the raw entry, for callers that need a
164
+ * real, unit-converted value (the gap calculation) — percentile ranking
165
+ * itself never needs this, since relative order is scale-invariant.
166
+ * @param {ReturnType<import("./model-capability-registry.js").createCapabilityRegistry>} registry
167
+ * @param {string} id
168
+ * @param {{metricAliases: Array<{metric: string, scale: string}>}} identity
169
+ */
170
+ function bestAcrossAliases(registry, id, identity) {
171
+ let best = null;
172
+ for (const alias of identity.metricAliases) {
173
+ for (const entry of registry.getEvidence(id, alias.metric)) {
174
+ const isBetter = !best
175
+ || (entry.verified && !best.entry.verified)
176
+ || (entry.verified === best.entry.verified && Date.parse(entry.date ?? "") > Date.parse(best.entry.date ?? ""));
177
+ // The evidence's OWN real scale (set by the ingestion source that
178
+ // actually knows it) always wins over the alias's metric-name-based
179
+ // guess — two sources can share one metric name ("hle") while
180
+ // genuinely reporting in different real scales (AA: 0-1 fraction;
181
+ // Hugging Face: 0-100, verified live — DeepSeek-V4.1-Flash's real
182
+ // HLE via HF is 63.9, not 0.639). The alias scale stays a fallback
183
+ // only for evidence that never declared its own.
184
+ if (isBetter) best = { entry, scale: entry.scale ?? alias.scale };
185
+ }
186
+ }
187
+ return best;
188
+ }
189
+
190
+ /**
191
+ * Percentile rank (0-1) of each real, comparable result for one benchmark
192
+ * identity, among the given candidate models — scale-invariant (only
193
+ * relative order matters, so 0-1 fractions and 0-100 scores from
194
+ * different sources never need unit conversion here). Requires at least
195
+ * two distinct models with a real result for this exact benchmark WHEN
196
+ * other real candidate models exist in the pool but simply lack evidence
197
+ * for this one benchmark (that reduces coverage honestly instead of
198
+ * inventing a comparison with nobody). The one exception: when the ENTIRE
199
+ * candidate pool is a single model (the only accessible real provider —
200
+ * a real, common case, not a data gap), that lone model gets percentile 1
201
+ * for any benchmark it has real evidence for — being the only real
202
+ * option available IS the real, correct result, not a fabricated one.
203
+ * @param {ReturnType<import("./model-capability-registry.js").createCapabilityRegistry>} registry
204
+ * @param {Array<object>} models
205
+ * @param {{metricAliases: Array<{metric: string, scale: string}>}} identity
206
+ * @returns {Map<string, {value: number, verified: boolean}>} modelKey -> percentile + whether the deciding real entry was independently verified
207
+ */
208
+ function percentileForBenchmark(registry, models, identity) {
209
+ const direction = directionFor(identity.metricAliases[0].metric);
210
+ const results = [];
211
+ for (const model of models) {
212
+ const id = registry.registerIdentity(model.adapterId, model.modelId, model.displayName ?? null);
213
+ const best = bestAcrossAliases(registry, id, identity);
214
+ // Different models can win bestAcrossAliases via different metric
215
+ // aliases of the SAME real benchmark identity (e.g. one model's best
216
+ // evidence is a 0-1 "unit" alias, another's is a 0-100 "hundred"
217
+ // alias) — comparing their raw entry.value would rank 0.64 below 57.9
218
+ // even though 0.64 (64%) is actually ahead of 57.9/100 (57.9%).
219
+ // Percentile ranking is scale-invariant only WITHIN one common scale,
220
+ // so every result is normalized to the same 0-1 scale before sorting.
221
+ if (best) results.push({ model, entry: best.entry, normalizedValue: convertByScale(best.scale, best.entry.value) });
222
+ }
223
+ if (results.length < 2 && models.length > 1) return new Map();
224
+ if (!results.length) return new Map();
225
+
226
+ const sorted = [...results].sort((a, b) => (direction === "higher" ? a.normalizedValue - b.normalizedValue : b.normalizedValue - a.normalizedValue));
227
+ const percentiles = new Map();
228
+ const n = sorted.length;
229
+ sorted.forEach((entry, index) => {
230
+ // Ties share the same percentile (their shared rank position), so a
231
+ // real dead-heat is never arbitrarily broken by array order here —
232
+ // portfolio-level tiebreaks happen later, on the final score.
233
+ let rankIndex = index;
234
+ while (rankIndex > 0 && sorted[rankIndex - 1].normalizedValue === entry.normalizedValue) rankIndex -= 1;
235
+ const percentile = n > 1 ? rankIndex / (n - 1) : 1;
236
+ percentiles.set(modelKey(entry.model), { value: percentile, verified: entry.entry.verified });
237
+ });
238
+ return percentiles;
239
+ }
240
+
241
+ /**
242
+ * @param {number[]} values
243
+ */
244
+ function median(values) {
245
+ if (!values.length) return null;
246
+ const sorted = [...values].sort((a, b) => a - b);
247
+ const mid = Math.floor(sorted.length / 2);
248
+ return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
249
+ }
250
+
251
+ /**
252
+ * One capability's real percentile per model: the median across every
253
+ * real benchmark identity in that capability that produced a comparable
254
+ * cohort (see percentileForBenchmark) — median, not mean, so one real
255
+ * outlier benchmark can't swing the capability score on its own. Falls
256
+ * back to AA's own composite index (also percentile-ranked, same
257
+ * scale-invariance) ONLY when NOT ONE component benchmark produced any
258
+ * real cohort anywhere in the pool — never blended alongside real
259
+ * components.
260
+ * @param {ReturnType<import("./model-capability-registry.js").createCapabilityRegistry>} registry
261
+ * @param {Array<object>} models
262
+ * @param {Capability} capability
263
+ * @returns {Map<string, {percentile: number, benchmarkCount: number, verifiedCount: number}>}
264
+ */
265
+ export function computeCapabilityPercentile(registry, models, capability) {
266
+ const identities = BENCHMARK_IDENTITIES.filter((identity) => identity.capability === capability);
267
+ const perModel = new Map(models.map((m) => [modelKey(m), { percentiles: [], verifiedCount: 0 }]));
268
+ let anyComponentCohort = false;
269
+
270
+ for (const identity of identities) {
271
+ const percentiles = percentileForBenchmark(registry, models, identity);
272
+ if (!percentiles.size) continue;
273
+ anyComponentCohort = true;
274
+ for (const [key, { value, verified }] of percentiles) {
275
+ const bucket = perModel.get(key);
276
+ bucket.percentiles.push(value);
277
+ if (verified) bucket.verifiedCount += 1;
278
+ }
279
+ }
280
+
281
+ if (!anyComponentCohort && COMPOSITE_FALLBACKS[capability]) {
282
+ const fallbackIdentity = { metricAliases: [{ metric: COMPOSITE_FALLBACKS[capability], scale: "hundred" }] };
283
+ const percentiles = percentileForBenchmark(registry, models, fallbackIdentity);
284
+ for (const [key, { value, verified }] of percentiles) {
285
+ const bucket = perModel.get(key);
286
+ bucket.percentiles.push(value);
287
+ if (verified) bucket.verifiedCount += 1;
288
+ }
289
+ }
290
+
291
+ const result = new Map();
292
+ for (const [key, bucket] of perModel) {
293
+ if (!bucket.percentiles.length) continue; // no real, comparable evidence at all — absent, never zero
294
+ result.set(key, {
295
+ percentile: median(bucket.percentiles),
296
+ benchmarkCount: bucket.percentiles.length,
297
+ verifiedCount: bucket.verifiedCount
298
+ });
299
+ }
300
+ return result;
301
+ }
302
+
303
+ /**
304
+ * One capability's real, scale-normalized magnitude per model — the
305
+ * median of each real benchmark identity's own best real value, converted
306
+ * to a common 0-1 scale via that alias's real `scale`. Unlike
307
+ * computeCapabilityPercentile, this needs no 2-model cohort (it's each
308
+ * model's own real value, not a rank), so it keeps real granularity even
309
+ * for a lone runner-up — exactly what a near-equivalence band or
310
+ * capability floor needs to mean anything with only 2-3 real candidates.
311
+ * Falls back to AA's own composite index under the same real rule as the
312
+ * percentile side: only when NOT ONE component benchmark has any real
313
+ * value anywhere in the pool, never blended alongside real components.
314
+ * @param {ReturnType<import("./model-capability-registry.js").createCapabilityRegistry>} registry
315
+ * @param {Array<object>} models
316
+ * @param {Capability} capability
317
+ * @returns {Map<string, number>} modelKey -> real 0-1 magnitude
318
+ */
319
+ export function computeCapabilityGapValue(registry, models, capability) {
320
+ const identities = BENCHMARK_IDENTITIES.filter((identity) => identity.capability === capability);
321
+ const perModel = new Map(models.map((m) => [modelKey(m), []]));
322
+ let anyComponentValue = false;
323
+
324
+ for (const identity of identities) {
325
+ for (const model of models) {
326
+ const id = registry.registerIdentity(model.adapterId, model.modelId, model.displayName ?? null);
327
+ const best = bestAcrossAliases(registry, id, identity);
328
+ if (!best) continue;
329
+ anyComponentValue = true;
330
+ perModel.get(modelKey(model)).push(convertByScale(best.scale, best.entry.value));
331
+ }
332
+ }
333
+
334
+ if (!anyComponentValue && COMPOSITE_FALLBACKS[capability]) {
335
+ const fallbackIdentity = { metricAliases: [{ metric: COMPOSITE_FALLBACKS[capability], scale: "hundred" }] };
336
+ for (const model of models) {
337
+ const id = registry.registerIdentity(model.adapterId, model.modelId, model.displayName ?? null);
338
+ const best = bestAcrossAliases(registry, id, fallbackIdentity);
339
+ if (best) perModel.get(modelKey(model)).push(convertByScale(best.scale, best.entry.value));
340
+ }
341
+ }
342
+
343
+ const result = new Map();
344
+ for (const [key, values] of perModel) {
345
+ if (values.length) result.set(key, median(values));
346
+ }
347
+ return result;
348
+ }
349
+
350
+ /**
351
+ * A role's real, scale-normalized magnitude per model — the median across
352
+ * its relevant capabilities' own real gap values (see
353
+ * computeCapabilityGapValue). This is what model-intelligence.js's
354
+ * NEAR_EQUIVALENCE_BAND and EFFICIENT_CAPABILITY_FLOOR actually compare —
355
+ * never RoleEvaluation.capabilityPercentile, which only encodes rank
356
+ * order and collapses to {0, 0.5, 1} with Kairo's typical small candidate
357
+ * pools.
358
+ * @param {ReturnType<import("./model-capability-registry.js").createCapabilityRegistry>} registry
359
+ * @param {Array<object>} models
360
+ * @param {Capability[]} relevantCapabilities
361
+ * @returns {Map<string, number>} modelKey -> real 0-1 magnitude
362
+ */
363
+ export function computeRoleGapValue(registry, models, relevantCapabilities) {
364
+ const perCapability = relevantCapabilities.map((capability) => computeCapabilityGapValue(registry, models, capability));
365
+ const result = new Map();
366
+ for (const model of models) {
367
+ const key = modelKey(model);
368
+ const values = perCapability.map((m) => m.get(key)).filter((v) => v != null);
369
+ if (values.length) result.set(key, median(values));
370
+ }
371
+ return result;
372
+ }
373
+
374
+ /**
375
+ * @typedef {object} RoleEvaluation
376
+ * @property {string} role
377
+ * @property {number} capabilityPercentile - median across the role's relevant capabilities, each already a median across that capability's real benchmarks
378
+ * @property {number} coverage - fraction of the role's relevant capabilities that produced a real score (0-1)
379
+ * @property {"high"|"medium"|"low"} confidence
380
+ * @property {number} benchmarkCount - total real benchmark identities that contributed, across all relevant capabilities
381
+ * @property {Record<string, number>} capabilities - per-capability percentile, only for capabilities with real evidence
382
+ * @property {Record<string, number>} benchmarkCountsByCapability - this model's own real, distinct benchmark-identity count per relevant capability (see countModelBenchmarks) — never a source count (AA HLE + Hugging Face HLE is one benchmark, not two).
383
+ * @property {Record<string, number>} benchmarkCoverage - benchmarkCountsByCapability[capability] / activeBenchmarkCountForCapability(capability), 0-1, per relevant capability.
384
+ * @property {string[]} provisionalCapabilities - relevant capabilities where this model's real coverage falls below the comparability floor (isCapabilityComparable) — empty when every relevant capability clears it.
385
+ * @property {boolean} isProvisional - true whenever provisionalCapabilities is non-empty.
386
+ */
387
+
388
+ /**
389
+ * Confidence tiers, from real coverage and provenance only — never from
390
+ * the score's magnitude:
391
+ * high: >=70% of the role's relevant capabilities scored, AND at
392
+ * least one contributing benchmark was independently verified.
393
+ * medium: >=40% coverage, OR at least two distinct real benchmark
394
+ * identities contributed (even if coverage is thin).
395
+ * low: anything short of that.
396
+ * A provisional candidate (real benchmark-identity coverage below the
397
+ * comparability floor for at least one relevant capability — see
398
+ * isCapabilityComparable) can never reach "high", even with a real,
399
+ * independently-verified single data point: one verified benchmark is
400
+ * real evidence, but not YET broad enough evidence to be that confident.
401
+ */
402
+ function confidenceFor(coverage, benchmarkCount, verifiedCount, isProvisional = false) {
403
+ if (coverage >= 0.7 && verifiedCount >= 1 && !isProvisional) return "high";
404
+ if (coverage >= 0.4 || benchmarkCount >= 2) return "medium";
405
+ return "low";
406
+ }
407
+
408
+ /**
409
+ * Computes one RoleEvaluation per candidate model for a role defined by
410
+ * its relevant capabilities. A model with zero real evidence across every
411
+ * relevant capability gets no evaluation at all (absent from the
412
+ * returned map) — per explicit rule, it never competes for the role
413
+ * rather than being scored as if it were the weakest real option.
414
+ * @param {ReturnType<import("./model-capability-registry.js").createCapabilityRegistry>} registry
415
+ * @param {Array<object>} models
416
+ * @param {string} role
417
+ * @param {Capability[]} relevantCapabilities
418
+ * @returns {Map<string, RoleEvaluation>}
419
+ */
420
+ export function computeRoleEvaluations(registry, models, role, relevantCapabilities) {
421
+ const perCapability = relevantCapabilities.map((capability) => ({
422
+ capability, scores: computeCapabilityPercentile(registry, models, capability)
423
+ }));
424
+
425
+ const evaluations = new Map();
426
+ for (const model of models) {
427
+ const key = modelKey(model);
428
+ const capabilities = {};
429
+ let benchmarkCount = 0;
430
+ let verifiedCount = 0;
431
+ for (const { capability, scores } of perCapability) {
432
+ const entry = scores.get(key);
433
+ if (!entry) continue;
434
+ capabilities[capability] = entry.percentile;
435
+ benchmarkCount += entry.benchmarkCount;
436
+ verifiedCount += entry.verifiedCount;
437
+ }
438
+ const scoredCapabilities = Object.keys(capabilities);
439
+ if (!scoredCapabilities.length) continue; // no real primary evidence anywhere — doesn't compete for this role
440
+
441
+ // Benchmark-LEVEL coverage per relevant capability — independent of
442
+ // whether a percentile cohort formed (that's a ranking concern; this
443
+ // is "how much of the capability does THIS model's own evidence
444
+ // cover"). Counts real, distinct benchmark identities, never sources.
445
+ const benchmarkCountsByCapability = {};
446
+ const benchmarkCoverage = {};
447
+ const provisionalCapabilities = [];
448
+ for (const capability of Object.keys(capabilities)) {
449
+ const count = countModelBenchmarks(registry, model, capability);
450
+ const active = activeBenchmarkCountForCapability(capability);
451
+ benchmarkCountsByCapability[capability] = count;
452
+ benchmarkCoverage[capability] = active ? count / active : 0;
453
+ // A score of 0 real component-benchmark identities can only mean
454
+ // this capability's real score came entirely from the composite-
455
+ // index fallback (computeCapabilityPercentile's own pool-wide
456
+ // switch) — the best real signal available in that mode, coarse
457
+ // but not partial, so it's never "provisional" for lacking
458
+ // component benchmarks it structurally couldn't have used anyway.
459
+ // Only real, PARTIAL component-benchmark coverage (count > 0, but
460
+ // still below the comparability floor) is provisional.
461
+ if (count > 0 && !isCapabilityComparable(capability, count)) provisionalCapabilities.push(capability);
462
+ }
463
+ const isProvisional = provisionalCapabilities.length > 0;
464
+
465
+ const coverage = scoredCapabilities.length / relevantCapabilities.length;
466
+ evaluations.set(key, {
467
+ role,
468
+ capabilityPercentile: median(scoredCapabilities.map((c) => capabilities[c])),
469
+ coverage,
470
+ confidence: confidenceFor(coverage, benchmarkCount, verifiedCount, isProvisional),
471
+ benchmarkCount,
472
+ capabilities,
473
+ benchmarkCountsByCapability, benchmarkCoverage, provisionalCapabilities, isProvisional
474
+ });
475
+ }
476
+ return evaluations;
477
+ }
478
+
479
+ /** Ordinal rank so confidence can be compared/sorted (higher is more confident). */
480
+ export const CONFIDENCE_RANK = { high: 2, medium: 1, low: 0 };