@opennodes/registry 0.1.25 → 0.1.26

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/web/index.html +42 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opennodes/registry",
3
- "version": "0.1.25",
3
+ "version": "0.1.26",
4
4
  "description": "OpenNodes (ONP) reference registry: node registration with proof-of-resource qualification (Stage A/B/C), transparent ranking, enforceable estimates, catalog importers (OpenRouter, Hugging Face, models.dev, Ollama), MCP surface, and the web app (catalog, chat, advisor, Node Builder)",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -47,6 +47,13 @@
47
47
  .mitem .t .nm { font-weight: 600; font-size: 0.86rem; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
48
48
  .mitem .b { display: flex; gap: 0.8rem; margin-top: 0.25rem; font-size: 0.7rem; color: var(--muted); font-variant-numeric: tabular-nums; flex-wrap: wrap; }
49
49
  .mdetail { min-height: 30rem; }
50
+ .scorebox { margin: 0.4rem 0 0.8rem; border: 1px solid var(--line); border-radius: 6px; background: var(--panel); padding: 0.5rem 0.75rem; font-size: 0.8rem; }
51
+ .scorebox summary { cursor: pointer; }
52
+ .scorebox table { width: 100%; border-collapse: collapse; margin-top: 0.4rem; }
53
+ .scorebox td { padding: 0.2rem 0.5rem 0.2rem 0; border-top: 1px solid var(--line-2); vertical-align: top; }
54
+ .scorebox td.num { text-align: right; white-space: nowrap; }
55
+ .scorebox td.sub { font-size: 0.74rem; }
56
+ .scorebox tr.tot td { border-top: 1px solid var(--line); }
50
57
  .pager { display: flex; align-items: center; justify-content: center; gap: 0.3rem; margin-top: 0.5rem; font-size: 0.78rem; color: var(--muted); flex-wrap: wrap; }
51
58
  .pager:empty { display: none; }
52
59
  .pgbtn { font: inherit; min-width: 2rem; padding: 0.25rem 0.5rem; border-radius: 6px; border: 1px solid var(--line); background: var(--panel); color: var(--accent); cursor: pointer; }
@@ -267,6 +274,7 @@
267
274
  </div>
268
275
  <p class="desc" id="md-desc" hidden></p>
269
276
  <div class="statstrip" id="md-stats"></div>
277
+ <div id="md-score"></div>
270
278
  <div class="secnav" id="md-nav"></div>
271
279
  <h2 id="sec-offerings">Offerings <span class="counts" id="md-count"></span></h2>
272
280
  <div class="panel tablewrap" id="md-offerings"></div>
@@ -726,8 +734,10 @@ function renderAdvice(r) {
726
734
  });
727
735
  }
728
736
 
737
+ let listedOffering = null; // the list entry the detail was opened from: its score is explained there
729
738
  function selectModel(el) {
730
739
  document.querySelectorAll('.mitem').forEach((x) => x.classList.toggle('sel', x === el));
740
+ listedOffering = lastResults.find((o) => `${o.node_id}/${o.offering_id}` === el.dataset.key) ?? null;
731
741
  const host = $('#m-detail');
732
742
  if (!host.contains($('#model-card'))) { host.innerHTML = ''; host.appendChild($('#model-card')); }
733
743
  loadModel(el.dataset.artifact);
@@ -736,7 +746,7 @@ function selectModel(el) {
736
746
  let lastResults = [];
737
747
  function renderItem(o) {
738
748
  const obs = o.observed || {};
739
- return `<div class="mitem" data-artifact="${esc(o.model.artifact)}">
749
+ return `<div class="mitem" data-artifact="${esc(o.model.artifact)}" data-key="${esc(o.node_id + '/' + o.offering_id)}">
740
750
  <div class="t">
741
751
  <span class="nm">${esc(o.model.name)}</span>
742
752
  ${tierChip(o.tier)}
@@ -839,6 +849,35 @@ function renderRow(o, idx) {
839
849
  }
840
850
 
841
851
  /* ---------- model detail ---------- */
852
+ // How the list's ranking score for the selected entry was computed: weight × component per part.
853
+ function renderScore(artifact) {
854
+ const host = $('#md-score');
855
+ const o = listedOffering;
856
+ if (!o?.rank_explanation || o.model.artifact !== artifact) { host.innerHTML = ''; return; }
857
+ const { weights, components } = o.rank_explanation;
858
+ const obs = o.observed || {};
859
+ const why = {
860
+ price: o.pricing?.input_per_mtok == null ? 'no price published'
861
+ : `${o.pricing.input_per_mtok === 0 ? 'free' : '$' + fmt(o.pricing.input_per_mtok) + '/MTok in'} — position among the listed results (cheapest 1.00, dearest 0.00)`,
862
+ tier: {
863
+ verified: 'verified: passed identity, context and capacity checks',
864
+ community: 'community: passed the registration checks',
865
+ attested: 'attested by a third party',
866
+ }[o.tier] ?? (o.source === 'import' ? 'unverified: imported from a public catalog, not checked by this registry' : 'unverified: not checked yet'),
867
+ probe_success: obs.probes > 0 ? `${Math.round((obs.probe_success ?? 0) * obs.probes)} of the last ${obs.probes} checks passed` : 'no checks yet',
868
+ };
869
+ const label = { price: 'Price', tier: 'Trust tier', probe_success: 'Check pass rate' };
870
+ const rows = Object.keys(weights).map((k) => `<tr>
871
+ <td>${label[k] ?? esc(k)}</td>
872
+ <td class="num mono">${fmt(components[k] ?? 0)} × ${fmt(weights[k])}</td>
873
+ <td class="num mono"><b>${fmt((components[k] ?? 0) * weights[k])}</b></td>
874
+ <td class="sub">${esc(why[k] ?? '')}</td></tr>`).join('');
875
+ host.innerHTML = `<details class="scorebox" open>
876
+ <summary>Ranking score <b class="mono">${fmt(o.rank)}</b> <span class="sub">for ${esc(o.node_id)} in the current list</span></summary>
877
+ <table>${rows}<tr class="tot"><td>Total</td><td></td><td class="num mono"><b>${fmt(o.rank)}</b></td><td class="sub">weighted sum; measured values, never paid placement</td></tr></table>
878
+ </details>`;
879
+ }
880
+
842
881
  async function loadModel(artifact) {
843
882
  $('#model-card').dataset.artifact = artifact;
844
883
  $('#md-crumb').innerHTML = '<a href="#/">Models</a> / ' + esc(artifact);
@@ -892,6 +931,8 @@ async function loadModel(artifact) {
892
931
  first.modalities ? stat(esc((first.modalities.input || []).join('+')) + ' → ' + esc((first.modalities.output || []).join('+')), 'MODALITIES') : '',
893
932
  ].filter(Boolean).join('');
894
933
 
934
+ renderScore(artifact);
935
+
895
936
  // section nav (anchors appear only for sections that render)
896
937
  const navBits = [['#sec-offerings', 'OFFERINGS'], ['#sec-play', 'PLAYGROUND']];
897
938