omp-llm-role 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +202 -12
- package/agents/designer.md +4 -3
- package/docs/dev/README.md +44 -7
- package/docs/dev/agent-authoring.md +59 -1
- package/docs/dev/architecture.md +106 -28
- package/docs/dev/ci.md +163 -0
- package/docs/dev/current-state.md +364 -24
- package/docs/dev/data-sources.md +231 -38
- package/docs/dev/explorer.md +175 -46
- package/docs/dev/quirks.md +77 -4
- package/docs/dev/releasing.md +130 -17
- package/docs/dev/scoring.md +851 -27
- package/docs/dev/testing.md +253 -0
- package/docs/dev/writes-and-state.md +232 -28
- package/docs/llm-role-rankings.md +149 -148
- package/package.json +814 -80
- package/skills/omp-llm-role-create-agent/SKILL.md +7 -1
- package/src/agent-architect.ts +148 -19
- package/src/agent-create.ts +800 -40
- package/src/agent-file.ts +2 -5
- package/src/architect-provenance.ts +16 -0
- package/src/availability.ts +191 -15
- package/src/benchmark-author.ts +9 -4
- package/src/benchmark-sources.ts +283 -18
- package/src/cli/create-role.ts +29 -1
- package/src/cli/llm-role-rank.ts +40 -1
- package/src/config-edit.ts +207 -80
- package/src/engine.ts +519 -75
- package/src/error-report.ts +562 -0
- package/src/explorer/boot.ts +36 -24
- package/src/explorer/explain.ts +128 -13
- package/src/explorer/scopes.ts +110 -0
- package/src/explorer/server.ts +126 -33
- package/src/extension.ts +464 -48
- package/src/features.ts +105 -0
- package/src/project-registry.ts +65 -0
- package/src/project-setup.ts +382 -0
- package/src/prompts/agent-creation-architect.md +2 -0
- package/src/prompts/project-profile-architect.md +33 -0
- package/src/prompts/project-profile-user.md +8 -0
- package/src/provider-display.ts +67 -0
- package/src/role-archetypes.ts +12 -2
- package/src/role-settings.ts +22 -0
- package/src/settings.ts +211 -24
- package/src/state.ts +1 -1
- package/src/updater.ts +234 -37
- package/web/app.js +434 -12
- package/web/index.html +1 -0
- package/web/style.css +44 -1
- package/docs/dev/spec.md +0 -633
package/web/app.js
CHANGED
|
@@ -12,6 +12,7 @@ const state = {
|
|
|
12
12
|
defaults: {},
|
|
13
13
|
metrics: [],
|
|
14
14
|
metricMeta: {},
|
|
15
|
+
focusAssessments: {},
|
|
15
16
|
levels: [],
|
|
16
17
|
thinkingFactors: {},
|
|
17
18
|
rows: [],
|
|
@@ -20,8 +21,14 @@ const state = {
|
|
|
20
21
|
derivedLambda: 0,
|
|
21
22
|
errors: [],
|
|
22
23
|
lockPath: "",
|
|
24
|
+
scopes: [],
|
|
25
|
+
activeScope: "",
|
|
26
|
+
features: {},
|
|
27
|
+
featureRegistry: [],
|
|
23
28
|
filter: "",
|
|
24
29
|
topn: "25",
|
|
30
|
+
hideBlocked: false,
|
|
31
|
+
availability: null,
|
|
25
32
|
explain: null,
|
|
26
33
|
exportMessage: "",
|
|
27
34
|
};
|
|
@@ -109,7 +116,11 @@ const TIPS = {
|
|
|
109
116
|
models: "Models in the cached llm-stats leaderboard snapshot.",
|
|
110
117
|
fetched: "UTC day of the cached snapshot; a cache older than today is refetched on boot.",
|
|
111
118
|
orJoin: "OpenRouter join: matched = models paired by slug suffix (source of throughput and price); priced = matched models that carry an OpenRouter price.",
|
|
112
|
-
|
|
119
|
+
availability: "Whether your OpenRouter key can run each model, from the keyed catalog. Enable OpenRouter → Settings → \"Filter the model catalog for API keys\" so the keyed catalog is a per-model allowlist; otherwise every mark reads unknown.",
|
|
120
|
+
key: "Key usability: usable = your OpenRouter key can run this model (in the keyed catalog); blocked = the public catalog has it but your key cannot; unknown = the keyed catalog is inactive or the model is in neither catalog.",
|
|
121
|
+
hideBlocked: "Drop rows your OpenRouter key cannot run (key = blocked). Applied after the text filter and the top-n slice.",
|
|
122
|
+
lock: "Plugin settings lock file that Export writes to (the active scope's lock file).",
|
|
123
|
+
scope: "Role-config source: the user-level scope, or a project where /project-roles was used. Switching re-reads that scope's roles and universe from disk and points Export at its lock file. A project whose lock file is gone is unavailable.",
|
|
113
124
|
refresh: "Refetch llm-stats and OpenRouter data (hits the network).",
|
|
114
125
|
rank: "Rank among this role's eligible models, by value, descending.",
|
|
115
126
|
delta: "Rank change against the role's saved (effective) definition: + means this definition ranks the model higher than the plugin does today.",
|
|
@@ -132,7 +143,22 @@ const TIPS = {
|
|
|
132
143
|
lambda: "λ = price of one quality point, in $/M. Derived as " + LAMBDA_DERIVATION + " unless the role overrides lambda.",
|
|
133
144
|
sum: "Weights must sum to 1.0 (±0.01) or the plugin rejects the role.",
|
|
134
145
|
required: "Eligibility gate, not a weight: a model missing this metric is not ranked at all for the role.",
|
|
146
|
+
focus: "Nine-axis audit of each weighted benchmark/percentile metric, over the same pool the updater ranks on. A below-bar axis is warned; unknown means the metric's scores were not loaded (never a silent ok).",
|
|
147
|
+
focusCoverage: "Coverage: models carrying the metric (not the imputed fill) out of the pool, and the share. Below the coverage floor — or a capability-filled metric — is warned.",
|
|
148
|
+
focusDispersion: "Dispersion: IQR/median of the cardinal-normalized covered values. Too little spread means the metric barely separates models.",
|
|
149
|
+
focusComposition: "Composition: pool orgs above the minimum share that carry no covered model. A missing major provider is warned.",
|
|
150
|
+
focusFreshness: "Freshness: months between the newest covered model and the newest pool model. A stale benchmark is warned.",
|
|
151
|
+
focusTrust: "Trust: the self-reported share of the payload's covered entries (the per-entry self_reported flag). A mostly vendor-submitted source is warned; unknown means the payload carried no entry flags or the scores were not loaded.",
|
|
152
|
+
focusModality: "Modality: the catalog row's modality (text/image/audio/video/multimodal) and the payload's multimodal share. Informational only — never a gate; unknown means neither the catalog row nor the payload carried a modality signal.",
|
|
153
|
+
focusMaintenance: "Maintenance: months since the catalog row's updated_at (the dataset's own age), against a 12-month bar, with version_count/star_count as supporting detail. This is the dataset-date axis, distinct from freshness (the model-date axis). An annotation, never a gate — a stale dataset is warned but never drops a candidate; unknown means the catalog row is absent or its updated_at is missing/unparseable.",
|
|
154
|
+
focusProvenance: "Provenance: the benchmark's owner (the catalog row's dataset_org_id, falling back to dataset_slug) and the source's own per-entry org mix (the dominant lab and its share). A cross-check on the pool-derived composition axis: agree/disagree records whether the two concur. An annotation, never a gate — a vendor-populated source is warned but never drops a candidate; unknown means there is neither an owner nor a payload org mix.",
|
|
155
|
+
focusCrossSource: "Cross-source: the zeroeval per-entry price/context compared against the plugin's own price/context for the models carrying the metric (the median relative divergence, within a 0.5 tolerance). The speed figure is informational only (rps vs tok/s). An annotation, never a gate — it never feeds the 1/price² blend; unknown means no model joined or the payload carried none of the four fields.",
|
|
135
156
|
imageFilter: "Require image input (filters.image) — the gate that shrinks the vision role's eligible set.",
|
|
157
|
+
providerPin: "Hard provider pin: the OpenRouter provider slug this role's requests are routed to (a tiered slug like deepinfra/fp8 is accepted verbatim). Setting it opts the role out of the automatic best-route pin; empty = default OpenRouter routing.",
|
|
158
|
+
features: "Opt-in capability presets (issue #40). Each flag applies the plugin's recommended settings for that capability to this role; an explicit knob value always wins. Export writes the flag, not the expanded values, so a future change to the recommendation keeps reaching the role.",
|
|
159
|
+
featureState: "inherit = use the global flag (features.<id> in the lock file); on/off = a per-role override (roles.<role>.features.<id>) that beats the global in either direction.",
|
|
160
|
+
featureKnob: "An individual knob the capability's recommended bundle fills. Leave it at inherit to take the recommendation; set a value to override it (an explicit value always wins over the flag).",
|
|
161
|
+
featureDerived: "derived = this value comes from the enabled capability's recommended bundle, not from an explicit key in the role. Set the knob to make it authored.",
|
|
136
162
|
thinking: "Thinking level appended to the role's selector (`:level`) and used to scale the price axis. The factor (3ρ+1+T)/(3ρ+1) applies only to models that will actually run the level (omp catalog `thinking[]` membership; meta levels off/auto need only a non-empty list). Bare = no suffix — the session's defaultThinkingLevel applies and the price is unadjusted.",
|
|
137
163
|
thinkingBare: "Bare (no suffix). Not restorable once the role's shipped default or the lock file sets a level: the plugin deep-merges roles over DEFAULT_ROLES, so an omitted key keeps the inherited value.",
|
|
138
164
|
normalize: "Rescale every weight so the sum is 1.0 (the parked ε is rescaled too).",
|
|
@@ -211,18 +237,52 @@ function initTips() {
|
|
|
211
237
|
// Header + role tabs
|
|
212
238
|
// ---------------------------------------------------------------------------
|
|
213
239
|
|
|
240
|
+
/** Header availability summary. When the keyed catalog is active it reports the
|
|
241
|
+
* allowlist size; otherwise it names the OpenRouter setting that turns it on and
|
|
242
|
+
* every row's mark reads unknown. */
|
|
243
|
+
function availabilitySpan() {
|
|
244
|
+
const a = state.availability;
|
|
245
|
+
if (a && a.active) {
|
|
246
|
+
return el("span", { "data-tip": TIPS.availability, text: "key: " + a.keyedCount + " usable / " + a.blockedCount + " blocked" });
|
|
247
|
+
}
|
|
248
|
+
return el("span", { class: "muted", "data-tip": TIPS.availability, text: "availability unknown — enable OpenRouter → Settings → \"Filter the model catalog for API keys\"" });
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** The scope selector: the user-level scope plus every known project. A scope
|
|
252
|
+
* whose lock file is gone is disabled with a reason tooltip. */
|
|
253
|
+
function scopeSelect() {
|
|
254
|
+
const sel = el("select", { id: "scope", "data-tip": TIPS.scope });
|
|
255
|
+
for (const s of state.scopes) {
|
|
256
|
+
sel.append(
|
|
257
|
+
el("option", {
|
|
258
|
+
value: s.id,
|
|
259
|
+
text: s.label,
|
|
260
|
+
disabled: s.present ? null : "",
|
|
261
|
+
title: s.present ? null : "unavailable — lock file missing",
|
|
262
|
+
}),
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
sel.value = state.activeScope;
|
|
266
|
+
sel.addEventListener("change", onScopeChange);
|
|
267
|
+
return sel;
|
|
268
|
+
}
|
|
269
|
+
|
|
214
270
|
function renderMeta(data) {
|
|
215
271
|
const meta = document.getElementById("meta");
|
|
216
272
|
meta.textContent = "";
|
|
217
273
|
meta.append(
|
|
218
274
|
el("span", { class: "brand", text: "omp-llm-role explorer" }),
|
|
219
275
|
el("span", { class: "sep", text: "·" }),
|
|
276
|
+
scopeSelect(),
|
|
277
|
+
el("span", { class: "sep", text: "·" }),
|
|
220
278
|
el("span", { "data-tip": TIPS.models, text: data.modelCount + " models" }),
|
|
221
279
|
el("span", { class: "sep", text: "·" }),
|
|
222
280
|
el("span", { "data-tip": TIPS.fetched, text: "fetched " + data.fetchedAt.slice(0, 10) }),
|
|
223
281
|
el("span", { class: "sep", text: "·" }),
|
|
224
282
|
el("span", { "data-tip": TIPS.orJoin, text: "OpenRouter " + data.orMatched + " matched / " + data.orPriced + " priced" }),
|
|
225
283
|
el("span", { class: "sep", text: "·" }),
|
|
284
|
+
availabilitySpan(),
|
|
285
|
+
el("span", { class: "sep", text: "·" }),
|
|
226
286
|
el("span", { class: "lock", "data-tip": TIPS.lock, text: "lock: " + data.lockPath }),
|
|
227
287
|
el("button", { id: "refresh", "data-tip": TIPS.refresh, text: "Refresh data", onclick: onRefresh }),
|
|
228
288
|
);
|
|
@@ -337,6 +397,21 @@ async function recompute() {
|
|
|
337
397
|
}
|
|
338
398
|
}
|
|
339
399
|
|
|
400
|
+
/** Tooltip text for one key verdict. The server owns the verdict; the client
|
|
401
|
+
* only explains it, so the three states cannot drift from the ranking. */
|
|
402
|
+
function keyTip(key) {
|
|
403
|
+
if (key === "usable") return "usable — your OpenRouter key can run this model (in the keyed catalog)";
|
|
404
|
+
if (key === "blocked") return "key-blocked — the public catalog has this model but your key cannot run it (OpenRouter → Settings → \"Filter the model catalog for API keys\")";
|
|
405
|
+
return "unknown — the keyed catalog is inactive or this model is in neither catalog (OpenRouter → Settings → \"Filter the model catalog for API keys\")";
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** Three-state key badge. Renders exactly the verdict the server sent; the
|
|
409
|
+
* client never invents an availability rule. */
|
|
410
|
+
function keyBadge(key) {
|
|
411
|
+
const state_ = key === "usable" || key === "blocked" ? key : "unknown";
|
|
412
|
+
return el("span", { class: "badge " + state_, "data-tip": keyTip(state_), text: state_ });
|
|
413
|
+
}
|
|
414
|
+
|
|
340
415
|
function renderTable() {
|
|
341
416
|
const table = document.getElementById("table");
|
|
342
417
|
table.textContent = "";
|
|
@@ -347,6 +422,7 @@ function renderTable() {
|
|
|
347
422
|
el("th", { "data-tip": TIPS.frontier, text: "★" }),
|
|
348
423
|
el("th", { text: "model" }),
|
|
349
424
|
el("th", { text: "org" }),
|
|
425
|
+
el("th", { "data-tip": TIPS.key, text: "key" }),
|
|
350
426
|
el("th", { class: "num", "data-tip": TIPS.value, text: "value" }),
|
|
351
427
|
el("th", { class: "num", "data-tip": TIPS.q, text: "q" }),
|
|
352
428
|
el("th", { class: "num", "data-tip": TIPS.price, text: "$/M" }),
|
|
@@ -359,6 +435,9 @@ function renderTable() {
|
|
|
359
435
|
let rows = state.rows;
|
|
360
436
|
if (filter) rows = rows.filter((r) => (r.name + " " + r.org + " " + r.id).toLowerCase().includes(filter));
|
|
361
437
|
if (state.topn !== "all") rows = rows.slice(0, Number(state.topn));
|
|
438
|
+
// Order: text filter → top-n slice → hide-blocked drop. The drop runs last so
|
|
439
|
+
// the top-n count is the ranking's own prefix, not a post-filter count.
|
|
440
|
+
if (state.hideBlocked) rows = rows.filter((r) => r.key !== "blocked");
|
|
362
441
|
for (const r of rows) {
|
|
363
442
|
tbody.append(
|
|
364
443
|
el("tr", { class: "row" + (r.id === state.selectedId ? " selected" : ""), onclick: () => selectModel(r.id) }, [
|
|
@@ -367,6 +446,7 @@ function renderTable() {
|
|
|
367
446
|
el("td", { class: "star", text: r.frontier ? "★" : "" }),
|
|
368
447
|
el("td", { class: "model", text: r.name }),
|
|
369
448
|
el("td", { class: "org", text: r.org }),
|
|
449
|
+
el("td", { class: "key" }, keyBadge(r.key)),
|
|
370
450
|
el("td", { class: "num", text: fmt(r.value, 3) }),
|
|
371
451
|
el("td", { class: "num", text: fmt(r.q, 3) }),
|
|
372
452
|
el("td", { class: "num", text: fmt(r.priceEff, 2) }),
|
|
@@ -422,6 +502,21 @@ function renderExplain() {
|
|
|
422
502
|
}),
|
|
423
503
|
);
|
|
424
504
|
|
|
505
|
+
// Key verdict line: the server's overlay verdict, plus the reason and the
|
|
506
|
+
// OpenRouter setting name when the keyed catalog is not active.
|
|
507
|
+
let keyLine = "key: " + keyTip(ex.key);
|
|
508
|
+
if (ex.keyReason !== "active") keyLine += " (reason: " + ex.keyReason + ")";
|
|
509
|
+
panel.append(el("p", { class: "sub key-line", "data-tip": TIPS.key, text: keyLine }));
|
|
510
|
+
|
|
511
|
+
// The pin basis: a manual pin (from the edited def) overrides the automatic
|
|
512
|
+
// best-route pin the updater would write. Read-only here.
|
|
513
|
+
const manualPin = state.defs[state.role]?.providerPin;
|
|
514
|
+
if (manualPin) {
|
|
515
|
+
panel.append(el("p", { class: "sub pin-line", "data-tip": TIPS.providerPin, text: "pin: " + manualPin + " (manual)" }));
|
|
516
|
+
} else if (ex.autoPin) {
|
|
517
|
+
panel.append(el("p", { class: "sub pin-line", "data-tip": TIPS.providerPin, text: "auto pin: " + ex.autoPin }));
|
|
518
|
+
}
|
|
519
|
+
|
|
425
520
|
panel.append(el("h3", { "data-tip": TIPS.composition, text: "Value composition" }));
|
|
426
521
|
const comp = el("table", { class: "comp" });
|
|
427
522
|
comp.append(
|
|
@@ -463,7 +558,9 @@ function renderExplain() {
|
|
|
463
558
|
const derived = ex.role.lambda === ex.role.derivedLambda;
|
|
464
559
|
panel.append(el("p", { class: "cost", "data-tip": TIPS.lambda, text: "λ = " + ex.role.lambda.toFixed(5) + " $/quality-point" + (derived ? " (derived = " + LAMBDA_DERIVATION + ")" : " (override)") }));
|
|
465
560
|
const thinkNote = ex.role.thinking === undefined ? "bare" : ":" + ex.role.thinking;
|
|
466
|
-
|
|
561
|
+
const cacheNote = ex.role.cacheHitRate > 0 ? " · cache " + ex.role.cacheHitRate : "";
|
|
562
|
+
const ownNote = ex.ownProvider ? " · best route" : "";
|
|
563
|
+
panel.append(el("p", { class: "cost", "data-tip": TIPS.costPenalty, text: "eff price $" + fmt(ex.cost.priceEff, 2) + "/M" + (ex.cost.priceEff !== ex.cost.billedPrice ? " (billed $" + fmt(ex.cost.billedPrice, 2) + " × " + thinkNote + ")" : " (" + thinkNote + ")") + cacheNote + ownNote + " · penalty = λ·price = " + fmt(ex.cost.penalty, 4) + " · value = q − penalty = " + fmt(ex.cost.value, 4) }));
|
|
467
564
|
|
|
468
565
|
panel.append(el("h3", { "data-tip": TIPS.whyNotHigher, text: "Why not higher" }));
|
|
469
566
|
if (ex.gapAbove === null) {
|
|
@@ -531,6 +628,269 @@ function normalizeWeights(def) {
|
|
|
531
628
|
for (const key of Object.keys(def.weights)) def.weights[key] = def.weights[key] / sum;
|
|
532
629
|
}
|
|
533
630
|
|
|
631
|
+
/** The role's focus metrics: the weighted benchmark/percentile metrics, the same
|
|
632
|
+
* rule the server's focusMetricsOf applies (shipped roles weight index metrics,
|
|
633
|
+
* so they carry none). */
|
|
634
|
+
function focusMetricsOf(def) {
|
|
635
|
+
return Object.keys(def.weights).filter((metric) => {
|
|
636
|
+
const meta = state.metricMeta[metric];
|
|
637
|
+
return !!meta && (meta.kind === "benchmark" || meta.kind === "percentile");
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/** One focus-signal cell: the value text plus a status class. `unknown` renders
|
|
642
|
+
* as unknown (muted), never as ok; `below-bar` gets the warning tint and glyph. */
|
|
643
|
+
function focusSignal(text, status) {
|
|
644
|
+
const cls = status === "ok" ? "ok" : status === "below-bar" ? "below-bar" : "unknown";
|
|
645
|
+
return el("td", { class: "sig " + cls, text });
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/** The nine-axis focus assessment of the role's weighted benchmark/percentile
|
|
649
|
+
* metrics, from the bootstrap payload's focusAssessments[role]. The payload is
|
|
650
|
+
* computed over the resolved definition, so a metric the editor just added reads
|
|
651
|
+
* "not assessed" until it is Exported and the page reloaded. */
|
|
652
|
+
function renderFocus(panel) {
|
|
653
|
+
const def = state.defs[state.role];
|
|
654
|
+
const assessments = state.focusAssessments[state.role] || {};
|
|
655
|
+
const metrics = focusMetricsOf(def);
|
|
656
|
+
panel.append(el("h3", { "data-tip": TIPS.focus, text: "Focus metrics" }));
|
|
657
|
+
if (metrics.length === 0) {
|
|
658
|
+
panel.append(el("p", { class: "muted", text: "No benchmark/percentile metric weighted — the role ranks on the index/price/throughput backbone." }));
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
const table = el("table", { class: "focus" });
|
|
662
|
+
table.append(
|
|
663
|
+
el("thead", {}, el("tr", {}, [
|
|
664
|
+
el("th", { text: "metric" }),
|
|
665
|
+
el("th", { "data-tip": TIPS.focusCoverage, text: "coverage" }),
|
|
666
|
+
el("th", { "data-tip": TIPS.focusDispersion, text: "dispersion" }),
|
|
667
|
+
el("th", { "data-tip": TIPS.focusComposition, text: "composition" }),
|
|
668
|
+
el("th", { "data-tip": TIPS.focusFreshness, text: "freshness" }),
|
|
669
|
+
el("th", { "data-tip": TIPS.focusTrust, text: "trust" }),
|
|
670
|
+
el("th", { "data-tip": TIPS.focusModality, text: "modality" }),
|
|
671
|
+
el("th", { "data-tip": TIPS.focusMaintenance, text: "maintenance" }),
|
|
672
|
+
el("th", { "data-tip": TIPS.focusProvenance, text: "provenance" }),
|
|
673
|
+
el("th", { "data-tip": TIPS.focusCrossSource, text: "cross-source" }),
|
|
674
|
+
])),
|
|
675
|
+
);
|
|
676
|
+
const body = el("tbody");
|
|
677
|
+
for (const metric of metrics) {
|
|
678
|
+
const a = assessments[metric];
|
|
679
|
+
if (!a) {
|
|
680
|
+
body.append(
|
|
681
|
+
el("tr", {}, [
|
|
682
|
+
el("td", { "data-tip": metricTip(metric), text: metricLabel(metric) }),
|
|
683
|
+
el("td", { class: "sig unknown", colspan: "9", text: "not assessed — Export, then reload to assess the edited definition" }),
|
|
684
|
+
]),
|
|
685
|
+
);
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
const c = a.coverage;
|
|
689
|
+
const coverage = c.status === "unknown" ? "unknown" : c.covered + "/" + c.total + " (" + (c.share === null ? "?" : (c.share * 100).toFixed(1) + "%") + ")";
|
|
690
|
+
const dispersion = a.dispersion.status === "unknown" ? "unknown" : a.dispersion.value.toFixed(3);
|
|
691
|
+
const composition = a.composition.status === "unknown" ? "unknown" : a.composition.status === "ok" ? "ok" : "omits " + a.composition.omittedOrgs.join(", ");
|
|
692
|
+
const freshness = a.freshness.status === "unknown" ? "unknown" : a.freshness.monthsBehind.toFixed(1) + "mo";
|
|
693
|
+
const community = a.trust.community ?? false;
|
|
694
|
+
const trust =
|
|
695
|
+
a.trust.status === "unknown"
|
|
696
|
+
? community ? "community" : "unknown"
|
|
697
|
+
: a.trust.selfReported + "/" + a.trust.covered + " " + a.trust.status + (community ? " community" : "");
|
|
698
|
+
const modality =
|
|
699
|
+
a.modality.status === "unknown"
|
|
700
|
+
? "unknown"
|
|
701
|
+
: (a.modality.value ?? "?") + (a.modality.multimodalShare === null ? "" : " " + (a.modality.multimodalShare * 100).toFixed(0) + "% multimodal");
|
|
702
|
+
const maintenance = a.maintenance.status === "unknown" ? "unknown" : a.maintenance.monthsOld.toFixed(1) + "mo";
|
|
703
|
+
const provenance =
|
|
704
|
+
a.provenance.status === "unknown"
|
|
705
|
+
? "unknown"
|
|
706
|
+
: (a.provenance.owner ?? "?") +
|
|
707
|
+
(a.provenance.dominantShare === null ? "" : " " + (a.provenance.dominantShare * 100).toFixed(0) + "% " + (a.provenance.dominantOrg ?? "?")) +
|
|
708
|
+
" " +
|
|
709
|
+
a.provenance.compositionAgreement;
|
|
710
|
+
const crossSource =
|
|
711
|
+
a.crossSource.status === "unknown"
|
|
712
|
+
? "unknown"
|
|
713
|
+
: a.crossSource.compared +
|
|
714
|
+
" priceΔ" +
|
|
715
|
+
(a.crossSource.priceDivergence === null ? "?" : (a.crossSource.priceDivergence * 100).toFixed(0) + "%") +
|
|
716
|
+
" ctxΔ" +
|
|
717
|
+
(a.crossSource.contextDivergence === null ? "?" : (a.crossSource.contextDivergence * 100).toFixed(0) + "%");
|
|
718
|
+
const statuses = [c.status, a.dispersion.status, a.composition.status, a.freshness.status, a.trust.status, a.modality.status, a.maintenance.status, a.provenance.status, a.crossSource.status];
|
|
719
|
+
body.append(
|
|
720
|
+
el("tr", { class: statuses.includes("below-bar") ? "warned" : "" }, [
|
|
721
|
+
el("td", { "data-tip": metricTip(metric), text: metricLabel(metric) }),
|
|
722
|
+
focusSignal(coverage, c.status),
|
|
723
|
+
focusSignal(dispersion, a.dispersion.status),
|
|
724
|
+
focusSignal(composition, a.composition.status),
|
|
725
|
+
focusSignal(freshness, a.freshness.status),
|
|
726
|
+
focusSignal(trust, a.trust.status),
|
|
727
|
+
focusSignal(modality, a.modality.status),
|
|
728
|
+
focusSignal(maintenance, a.maintenance.status),
|
|
729
|
+
focusSignal(provenance, a.provenance.status),
|
|
730
|
+
focusSignal(crossSource, a.crossSource.status),
|
|
731
|
+
]),
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
table.append(body);
|
|
735
|
+
panel.append(table);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// ---------------------------------------------------------------------------
|
|
739
|
+
// Capability flags (issue #40)
|
|
740
|
+
// ---------------------------------------------------------------------------
|
|
741
|
+
|
|
742
|
+
/** The knobs a capability's recommended bundle fills, derived from the registry
|
|
743
|
+
* so the panel cannot drift from `src/features.ts`. */
|
|
744
|
+
function bundleKnobs(recommended) {
|
|
745
|
+
const out = [];
|
|
746
|
+
if (recommended.filters) {
|
|
747
|
+
for (const [key, value] of Object.entries(recommended.filters)) {
|
|
748
|
+
out.push({ key: "filters." + key, kind: typeof value === "boolean" ? "boolean" : "number", value });
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
if (recommended.cacheHitRate !== undefined) out.push({ key: "cacheHitRate", kind: "number", value: recommended.cacheHitRate });
|
|
752
|
+
if (recommended.preferOwnProvider !== undefined) out.push({ key: "preferOwnProvider", kind: "boolean", value: recommended.preferOwnProvider });
|
|
753
|
+
return out;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/** Render a recommended bundle as `key=value` pairs (mirrors formatBundle). */
|
|
757
|
+
function bundleText(recommended) {
|
|
758
|
+
return bundleKnobs(recommended).map((k) => k.key + "=" + k.value).join(", ");
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/** Read one knob from the AUTHORED def by its dotted key. */
|
|
762
|
+
function getKnob(def, key) {
|
|
763
|
+
if (key === "cacheHitRate") return def.cacheHitRate;
|
|
764
|
+
if (key === "preferOwnProvider") return def.preferOwnProvider;
|
|
765
|
+
if (key.startsWith("filters.")) return def.filters ? def.filters[key.slice("filters.".length)] : undefined;
|
|
766
|
+
return undefined;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/** Write (or clear, with `undefined`) one knob on the AUTHORED def. */
|
|
770
|
+
function setKnob(def, key, value) {
|
|
771
|
+
if (key === "cacheHitRate" || key === "preferOwnProvider") {
|
|
772
|
+
if (value === undefined) delete def[key];
|
|
773
|
+
else def[key] = value;
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
if (key.startsWith("filters.")) {
|
|
777
|
+
const leaf = key.slice("filters.".length);
|
|
778
|
+
if (value === undefined) {
|
|
779
|
+
if (def.filters) {
|
|
780
|
+
delete def.filters[leaf];
|
|
781
|
+
if (Object.keys(def.filters).length === 0) delete def.filters;
|
|
782
|
+
}
|
|
783
|
+
} else {
|
|
784
|
+
def.filters = def.filters || {};
|
|
785
|
+
def.filters[leaf] = value;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/** The effective state of one capability for a role: the authored per-role flag
|
|
791
|
+
* when present (it wins either way), else the scope's global flag. */
|
|
792
|
+
function effectiveFlag(def, id) {
|
|
793
|
+
const authored = def.features ? def.features[id] : undefined;
|
|
794
|
+
if (authored !== undefined) return authored;
|
|
795
|
+
return state.features[id] === true;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/** The effective value of one knob and whether it is DERIVED (filled by an
|
|
799
|
+
* enabled capability) rather than authored (an explicit key on the def). */
|
|
800
|
+
function effectiveKnob(def, featureId, knob) {
|
|
801
|
+
const authored = getKnob(def, knob.key);
|
|
802
|
+
if (authored !== undefined) return { value: authored, derived: false };
|
|
803
|
+
if (effectiveFlag(def, featureId) && knob.value !== undefined) return { value: knob.value, derived: true };
|
|
804
|
+
return { value: undefined, derived: false };
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/** One knob field: an editable input (number, or inherit/true/false select) plus
|
|
808
|
+
* a badge marking the value authored or derived from the capability. */
|
|
809
|
+
function knobField(def, featureId, knob) {
|
|
810
|
+
const authored = getKnob(def, knob.key);
|
|
811
|
+
const eff = effectiveKnob(def, featureId, knob);
|
|
812
|
+
const cell = el("span", { class: "knob" + (eff.derived ? " derived" : "") });
|
|
813
|
+
let input;
|
|
814
|
+
if (knob.kind === "boolean") {
|
|
815
|
+
input = el("select", { class: "knob-input", "data-tip": TIPS.featureKnob });
|
|
816
|
+
input.append(el("option", { value: "inherit", text: "inherit" }));
|
|
817
|
+
input.append(el("option", { value: "true", text: "true" }));
|
|
818
|
+
input.append(el("option", { value: "false", text: "false" }));
|
|
819
|
+
input.value = authored === undefined ? "inherit" : String(authored);
|
|
820
|
+
input.addEventListener("change", () => {
|
|
821
|
+
setKnob(def, knob.key, input.value === "inherit" ? undefined : input.value === "true");
|
|
822
|
+
renderEditor();
|
|
823
|
+
renderRoles();
|
|
824
|
+
scheduleRecompute();
|
|
825
|
+
});
|
|
826
|
+
} else {
|
|
827
|
+
input = el("input", { type: "number", class: "knob-input", step: "any", "data-tip": TIPS.featureKnob, placeholder: eff.value === undefined ? "—" : String(eff.value) });
|
|
828
|
+
if (authored !== undefined) input.value = String(authored);
|
|
829
|
+
input.addEventListener("input", () => {
|
|
830
|
+
setKnob(def, knob.key, input.value === "" ? undefined : Number(input.value));
|
|
831
|
+
scheduleRecompute();
|
|
832
|
+
});
|
|
833
|
+
input.addEventListener("change", () => {
|
|
834
|
+
renderEditor();
|
|
835
|
+
renderRoles();
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
cell.append(input);
|
|
839
|
+
cell.append(el("span", { class: "knob-name", text: knob.key }));
|
|
840
|
+
const badge = eff.derived ? "derived " + eff.value : authored !== undefined ? "authored" : "—";
|
|
841
|
+
cell.append(el("span", { class: "knob-badge " + (eff.derived ? "derived" : authored !== undefined ? "authored" : "none"), "data-tip": TIPS.featureDerived, text: badge }));
|
|
842
|
+
return cell;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/** The per-role Features panel: one tri-state control per capability (inherit /
|
|
846
|
+
* on / off) writing `def.features[id]`, the recommended bundle, and the
|
|
847
|
+
* individual knobs the bundle fills, each marked derived or authored. */
|
|
848
|
+
function renderFeatures(panel) {
|
|
849
|
+
panel.append(el("h3", { "data-tip": TIPS.features, text: "Capabilities" }));
|
|
850
|
+
if (state.featureRegistry.length === 0) {
|
|
851
|
+
panel.append(el("p", { class: "muted", text: "No capability registry in the payload." }));
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
const def = state.defs[state.role];
|
|
855
|
+
const wrap = el("div", { class: "features" });
|
|
856
|
+
for (const feature of state.featureRegistry) {
|
|
857
|
+
const authored = def.features ? def.features[feature.id] : undefined;
|
|
858
|
+
const on = effectiveFlag(def, feature.id);
|
|
859
|
+
const row = el("div", { class: "feature" + (on ? " on" : "") });
|
|
860
|
+
const sel = el("select", { class: "feature-state", "data-tip": TIPS.featureState });
|
|
861
|
+
sel.append(el("option", { value: "inherit", text: "inherit" }));
|
|
862
|
+
sel.append(el("option", { value: "on", text: "on" }));
|
|
863
|
+
sel.append(el("option", { value: "off", text: "off" }));
|
|
864
|
+
sel.value = authored === undefined ? "inherit" : authored ? "on" : "off";
|
|
865
|
+
sel.addEventListener("change", () => {
|
|
866
|
+
def.features = def.features || {};
|
|
867
|
+
if (sel.value === "inherit") {
|
|
868
|
+
delete def.features[feature.id];
|
|
869
|
+
if (Object.keys(def.features).length === 0) delete def.features;
|
|
870
|
+
} else {
|
|
871
|
+
def.features[feature.id] = sel.value === "on";
|
|
872
|
+
}
|
|
873
|
+
renderEditor();
|
|
874
|
+
renderRoles();
|
|
875
|
+
scheduleRecompute();
|
|
876
|
+
});
|
|
877
|
+
const origin = authored === undefined ? (state.features[feature.id] === true ? "global on" : "global off") : "role override";
|
|
878
|
+
row.append(
|
|
879
|
+
el("div", { class: "feature-head" }, [
|
|
880
|
+
sel,
|
|
881
|
+
el("span", { class: "feature-label", "data-tip": feature.description + "\n\n" + feature.buys, text: feature.label }),
|
|
882
|
+
el("span", { class: "feature-origin muted", text: origin }),
|
|
883
|
+
]),
|
|
884
|
+
);
|
|
885
|
+
row.append(el("div", { class: "feature-bundle muted", text: "recommended: " + bundleText(feature.recommended) }));
|
|
886
|
+
const knobs = el("div", { class: "feature-knobs" });
|
|
887
|
+
for (const knob of bundleKnobs(feature.recommended)) knobs.append(knobField(def, feature.id, knob));
|
|
888
|
+
row.append(knobs);
|
|
889
|
+
wrap.append(row);
|
|
890
|
+
}
|
|
891
|
+
panel.append(wrap);
|
|
892
|
+
}
|
|
893
|
+
|
|
534
894
|
function renderEditor() {
|
|
535
895
|
const panel = document.getElementById("editor");
|
|
536
896
|
panel.textContent = "";
|
|
@@ -650,6 +1010,8 @@ function renderEditor() {
|
|
|
650
1010
|
}
|
|
651
1011
|
panel.append(checks);
|
|
652
1012
|
|
|
1013
|
+
renderFocus(panel);
|
|
1014
|
+
|
|
653
1015
|
const imgCb = el("input", { type: "checkbox" });
|
|
654
1016
|
imgCb.checked = !!(def.filters && def.filters.image);
|
|
655
1017
|
imgCb.addEventListener("change", () => {
|
|
@@ -659,6 +1021,21 @@ function renderEditor() {
|
|
|
659
1021
|
});
|
|
660
1022
|
panel.append(el("label", { class: "check" }, [imgCb, el("span", { "data-tip": TIPS.imageFilter, text: "requires image input (filters.image)" })]));
|
|
661
1023
|
|
|
1024
|
+
panel.append(el("h3", { "data-tip": TIPS.providerPin, text: "Provider pin" }));
|
|
1025
|
+
const pin = el("input", { type: "text", class: "desc", "data-tip": TIPS.providerPin, value: def.providerPin || "", placeholder: "provider slug, e.g. deepinfra/fp8" });
|
|
1026
|
+
pin.addEventListener("change", () => {
|
|
1027
|
+
// Empty clears the key (default routing); a tiered slug is kept verbatim.
|
|
1028
|
+
const value = pin.value.trim();
|
|
1029
|
+
if (value === "") delete def.providerPin;
|
|
1030
|
+
else def.providerPin = value;
|
|
1031
|
+
renderEditor();
|
|
1032
|
+
renderRoles();
|
|
1033
|
+
scheduleRecompute();
|
|
1034
|
+
});
|
|
1035
|
+
panel.append(el("div", { class: "row-controls" }, [pin]));
|
|
1036
|
+
|
|
1037
|
+
renderFeatures(panel);
|
|
1038
|
+
|
|
662
1039
|
panel.append(el("h3", { "data-tip": TIPS.thinking, text: "Thinking level" }));
|
|
663
1040
|
const bareLocked = state.effective[state.role]?.thinking !== undefined;
|
|
664
1041
|
const levelSel = el("select", { id: "thinking", "data-tip": TIPS.thinking });
|
|
@@ -746,7 +1123,7 @@ async function onExport() {
|
|
|
746
1123
|
try {
|
|
747
1124
|
const res = await api("/api/export", { roles: dirty });
|
|
748
1125
|
if (res.ok) {
|
|
749
|
-
state.exportMessage = "wrote " + res.roles.length + " role(s)" + (res.backupPath ? " — backup: " + res.backupPath : "");
|
|
1126
|
+
state.exportMessage = "wrote " + res.roles.length + " role(s) to " + (res.lockPath || state.lockPath) + (res.backupPath ? " — backup: " + res.backupPath : "");
|
|
750
1127
|
for (const role of res.roles) state.effective[role] = structuredClone(state.defs[role]);
|
|
751
1128
|
renderRoles();
|
|
752
1129
|
renderEditor();
|
|
@@ -785,20 +1162,40 @@ function onDownload() {
|
|
|
785
1162
|
async function onRefresh() {
|
|
786
1163
|
if (!confirm("Refetch the leaderboard and OpenRouter data? This hits the network.")) return;
|
|
787
1164
|
try {
|
|
788
|
-
|
|
789
|
-
renderMeta(data);
|
|
790
|
-
await recompute();
|
|
1165
|
+
applyBootstrap(await api("/api/refresh", {}));
|
|
791
1166
|
} catch (err) {
|
|
792
1167
|
alert("Refresh failed: " + err.message);
|
|
793
1168
|
}
|
|
794
1169
|
}
|
|
795
1170
|
|
|
1171
|
+
/** Switch the active scope. Unsaved edits are confirmed first (mirroring
|
|
1172
|
+
* Refresh); the returned payload rebuilds every per-scope panel. */
|
|
1173
|
+
async function onScopeChange(event) {
|
|
1174
|
+
const id = event.target.value;
|
|
1175
|
+
if (id === state.activeScope) return;
|
|
1176
|
+
if (Object.keys(dirtyRoles()).length > 0 && !confirm("Discard unsaved role edits and switch scope?")) {
|
|
1177
|
+
event.target.value = state.activeScope;
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
try {
|
|
1181
|
+
applyBootstrap(await api("/api/scope", { scope: id }));
|
|
1182
|
+
} catch (err) {
|
|
1183
|
+
alert("Scope switch failed: " + err.message);
|
|
1184
|
+
event.target.value = state.activeScope;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
796
1188
|
// ---------------------------------------------------------------------------
|
|
797
1189
|
// Boot
|
|
798
1190
|
// ---------------------------------------------------------------------------
|
|
799
1191
|
|
|
800
|
-
|
|
801
|
-
|
|
1192
|
+
/** Rebuild every panel from a bootstrap payload (boot, scope switch, refresh).
|
|
1193
|
+
* The per-scope editing state is rebuilt from the payload, so dirty tracking is
|
|
1194
|
+
* per scope; the previously selected role is kept when the new scope has it. */
|
|
1195
|
+
function applyBootstrap(data) {
|
|
1196
|
+
const previousRole = state.role;
|
|
1197
|
+
state.scopes = data.scopes || [];
|
|
1198
|
+
state.activeScope = data.activeScope;
|
|
802
1199
|
state.universe = data.universe;
|
|
803
1200
|
state.effective = {};
|
|
804
1201
|
for (const [name, entry] of Object.entries(state.universe)) state.effective[name] = entry.def;
|
|
@@ -806,12 +1203,34 @@ async function boot() {
|
|
|
806
1203
|
state.defaults = data.defaults;
|
|
807
1204
|
state.metrics = data.metrics;
|
|
808
1205
|
state.metricMeta = data.metricMeta;
|
|
1206
|
+
state.focusAssessments = data.focusAssessments || {};
|
|
809
1207
|
state.levels = data.levels;
|
|
810
1208
|
state.thinkingFactors = data.thinkingFactors;
|
|
811
1209
|
state.lockPath = data.lockPath;
|
|
812
|
-
|
|
1210
|
+
state.availability = data.availability;
|
|
1211
|
+
state.features = data.features || {};
|
|
1212
|
+
state.featureRegistry = data.featureRegistry || [];
|
|
1213
|
+
state.rows = [];
|
|
1214
|
+
state.selectedId = null;
|
|
1215
|
+
state.explain = null;
|
|
1216
|
+
state.errors = [];
|
|
1217
|
+
state.exportMessage = "";
|
|
813
1218
|
renderMeta(data);
|
|
814
1219
|
renderRoles();
|
|
1220
|
+
renderErrors();
|
|
1221
|
+
renderTable();
|
|
1222
|
+
const role = previousRole && state.defs[previousRole] ? previousRole : Object.keys(state.defs)[0];
|
|
1223
|
+
if (role) selectRole(role);
|
|
1224
|
+
else {
|
|
1225
|
+
state.role = null;
|
|
1226
|
+
renderEditor();
|
|
1227
|
+
renderExplain();
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
async function boot() {
|
|
1232
|
+
initTips();
|
|
1233
|
+
applyBootstrap(await api("/api/bootstrap"));
|
|
815
1234
|
|
|
816
1235
|
document.getElementById("filter").addEventListener("input", (e) => {
|
|
817
1236
|
state.filter = e.target.value;
|
|
@@ -821,6 +1240,12 @@ async function boot() {
|
|
|
821
1240
|
state.topn = e.target.value;
|
|
822
1241
|
renderTable();
|
|
823
1242
|
});
|
|
1243
|
+
const hideBlocked = document.getElementById("hide-blocked");
|
|
1244
|
+
hideBlocked.dataset.tip = TIPS.hideBlocked;
|
|
1245
|
+
hideBlocked.addEventListener("change", (e) => {
|
|
1246
|
+
state.hideBlocked = e.target.checked;
|
|
1247
|
+
renderTable();
|
|
1248
|
+
});
|
|
824
1249
|
const exportBtn = document.getElementById("export");
|
|
825
1250
|
exportBtn.dataset.tip = TIPS.exportBtn;
|
|
826
1251
|
exportBtn.addEventListener("click", onExport);
|
|
@@ -830,9 +1255,6 @@ async function boot() {
|
|
|
830
1255
|
const downloadBtn = document.getElementById("download");
|
|
831
1256
|
downloadBtn.dataset.tip = TIPS.download;
|
|
832
1257
|
downloadBtn.addEventListener("click", onDownload);
|
|
833
|
-
|
|
834
|
-
const first = Object.keys(state.defs)[0];
|
|
835
|
-
if (first) selectRole(first);
|
|
836
1258
|
}
|
|
837
1259
|
|
|
838
1260
|
boot().catch((err) => {
|
package/web/index.html
CHANGED