Anton Malykhin commited on
Commit
c85bc2e
·
1 Parent(s): 54bc653

feat: stabilize HF deploy, locale routing, and bucket caching

Browse files

- Pin pnpm 11.9.0 through Corepack in GitLab CI and Docker builds
- Add workspace pnpm safeguards for engine strictness and dependency verification warnings
- Validate required Hugging Face deploy CI variables before pushing to the Space repository
- Enable Paraglide URL/baseLocale strategy for generated runtime and Vite integration
- Localize main navigation, logo, error page, and fallback links so language-prefixed routes are preserved
- Cache Hugging Face bucket ranking, details, and visualization data by snapshot_id
- Reuse cached adapted datasets when the bucket manifest still points to the same snapshot
- Share the ranking snapshot across details and visualization fetches to avoid duplicate manifest/data requests
- Rewrite README in English with setup, environment, development, and validation instructions
- Split docs into explicit Russian and English versions with _ru and _en suffixes
- Add English translations for backend data contract and Hugging Face Space deployment docs

.gitlab-ci.yml CHANGED
@@ -16,6 +16,8 @@ validate:
16
  - .pnpm-store/
17
  before_script:
18
  - corepack enable
 
 
19
  - pnpm config set store-dir .pnpm-store
20
  - pnpm install --frozen-lockfile
21
  script:
@@ -32,6 +34,9 @@ deploy:huggingface:
32
  - when: never
33
  before_script:
34
  - apk add --no-cache ca-certificates git
 
 
 
35
  - git config --global user.email "${GITLAB_USER_EMAIL:-ci@hivetrace.local}"
36
  - git config --global user.name "${GITLAB_USER_NAME:-GitLab CI}"
37
  script:
 
16
  - .pnpm-store/
17
  before_script:
18
  - corepack enable
19
+ - corepack prepare pnpm@11.9.0 --activate
20
+ - pnpm --version
21
  - pnpm config set store-dir .pnpm-store
22
  - pnpm install --frozen-lockfile
23
  script:
 
34
  - when: never
35
  before_script:
36
  - apk add --no-cache ca-certificates git
37
+ - test -n "$HF_USERNAME" || (echo "HF_USERNAME CI variable is not set" && exit 1)
38
+ - test -n "$HF_SPACE_TOKEN" || (echo "HF_SPACE_TOKEN CI variable is not set" && exit 1)
39
+ - test -n "$HF_SPACE_REPO" || (echo "HF_SPACE_REPO CI variable is not set" && exit 1)
40
  - git config --global user.email "${GITLAB_USER_EMAIL:-ci@hivetrace.local}"
41
  - git config --global user.name "${GITLAB_USER_NAME:-GitLab CI}"
42
  script:
Dockerfile CHANGED
@@ -3,8 +3,10 @@ FROM node:24-alpine AS builder
3
 
4
  WORKDIR /app
5
 
 
 
6
  COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
7
- RUN corepack enable
8
  RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
9
  pnpm install --frozen-lockfile
10
 
 
3
 
4
  WORKDIR /app
5
 
6
+ ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0
7
+
8
  COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
9
+ RUN corepack enable && corepack prepare pnpm@11.9.0 --activate && pnpm --version
10
  RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
11
  pnpm install --frozen-lockfile
12
 
README.md CHANGED
@@ -15,79 +15,83 @@ tags:
15
 
16
  # HiveTrace Leaderboard frontend
17
 
18
- - #### Запуск
19
 
20
- _Необходима версия **Node.js** не ниже `20.19.5`._
 
21
 
22
- качестве менеджера зависимостей используется **pnpm** версии `11.9.0`._
23
 
24
- _Интерфейс написан на [**SvelteKit**](https://svelte.dev/)._
 
 
 
 
 
25
 
26
- качестве библиотеки компонентов используется [**shadcn-svelte**](https://www.shadcn-svelte.com/) + [**shadcn-svelte-extras**](https://www.shadcn-svelte-extras.com/) (знание [**Tailwind**](https://tailwindcss.com/) обязательно!)._
27
 
28
- _Для локализации используется библиотека [**Paraglide JS**](https://inlang.com/m/gerre34r/library-inlang-paraglideJs)._
29
- - Переходим в корень проекта (директория `leaderboard_frontend`)
30
 
31
- - Устанавливаем зависимости:
32
 
33
- ```bash
34
- pnpm install
35
- ```
36
 
37
- - Создаем локальный `.env` из примера и указываем read-токен с доступом к приватному
38
- Hugging Face bucket:
 
39
 
40
- ```bash
41
- cp .env.example .env
42
- ```
43
 
44
- Обязательные server-only переменные:
 
 
45
 
46
- ```env
47
- HF_TOKEN=hf_...
48
- HF_BUCKET_ID=hivetrace/leaderboard_frontend_v2
49
- HF_BUCKET_PREFIX=latest
50
- ```
51
 
52
- `HF_TOKEN` нельзя переименовывать в `PUBLIC_HF_TOKEN`: секрет используется только сервером
53
- SvelteKit. В production токен необходимо передавать как runtime secret, а не как Docker build
54
- argument.
55
 
56
- - Запуск в `dev`:
 
 
 
 
57
 
58
- ```bash
59
- pnpm dev
60
- ```
61
 
62
- **ВАЖНО! Перед пушем в удаленный репозиторий ОБЯЗАТЕЛЬНО необходимо выполнить форматирование кода, прогнать линтер, проверить уязвимости и сборку**
63
- - Выполняем автоматическое форматирование:
64
 
65
- ```bash
66
- pnpm format
67
- ```
68
 
69
- - Прогоняем линтер:
70
 
71
- _Если есть ошибки - исправляем. Варнинги можно не трогать, желательно просмотреть и задуматься!_
 
72
 
73
- ```bash
74
- pnpm lint
75
- ```
76
 
77
- - Проверяем SvelteKit, типы и Svelte-компоненты:
 
 
78
 
79
- _Если есть ошибки - исправляем до запуска сборки._
80
 
81
- ```bash
82
- pnpm check
83
- ```
84
 
85
- - Продакшн сборка:
86
 
87
- _Здесь смотрим, чтобы все собралось и не упало в процессе. (Упадет здесь - упадет и там!)_
 
 
88
 
89
- ```bash
90
- pnpm build
91
- ```
92
 
93
- - Если все хорошо, можно собрать все коммиты в один и пушить в репозиторий.
 
 
 
15
 
16
  # HiveTrace Leaderboard frontend
17
 
18
+ ## Overview
19
 
20
+ HiveTrace Leaderboard is a SvelteKit frontend for browsing guardrail benchmark results from
21
+ a private Hugging Face bucket.
22
 
23
+ The app uses:
24
 
25
+ - [SvelteKit](https://svelte.dev/)
26
+ - [pnpm](https://pnpm.io/) `11.9.0`
27
+ - [shadcn-svelte](https://www.shadcn-svelte.com/) and
28
+ [shadcn-svelte-extras](https://www.shadcn-svelte-extras.com/)
29
+ - [Tailwind CSS](https://tailwindcss.com/)
30
+ - [Paraglide JS](https://inlang.com/m/gerre34r/library-inlang-paraglideJs) for localization
31
 
32
+ ## Requirements
33
 
34
+ - Node.js `20.19.5` or newer
35
+ - pnpm `11.9.0`
36
 
37
+ ## Setup
38
 
39
+ From the project root:
 
 
40
 
41
+ ```bash
42
+ pnpm install
43
+ ```
44
 
45
+ Create a local `.env` file from the example:
 
 
46
 
47
+ ```bash
48
+ cp .env.example .env
49
+ ```
50
 
51
+ Then configure a read token with access to the private Hugging Face bucket.
 
 
 
 
52
 
53
+ Required server-only environment variables:
 
 
54
 
55
+ ```env
56
+ HF_TOKEN=hf_...
57
+ HF_BUCKET_ID=hivetrace/leaderboard_frontend_v2
58
+ HF_BUCKET_PREFIX=latest
59
+ ```
60
 
61
+ Do not rename `HF_TOKEN` to `PUBLIC_HF_TOKEN`. The token is used only on the SvelteKit server.
62
+ In production, pass it as a runtime secret, not as a Docker build argument.
 
63
 
64
+ ## Development
 
65
 
66
+ ```bash
67
+ pnpm dev
68
+ ```
69
 
70
+ ## Validation
71
 
72
+ Before pushing, format the code, run linting, run Svelte/TypeScript checks, and verify the
73
+ production build.
74
 
75
+ Format:
 
 
76
 
77
+ ```bash
78
+ pnpm format
79
+ ```
80
 
81
+ Lint:
82
 
83
+ ```bash
84
+ pnpm lint
85
+ ```
86
 
87
+ Check SvelteKit, TypeScript, and Svelte components:
88
 
89
+ ```bash
90
+ pnpm check
91
+ ```
92
 
93
+ Build:
 
 
94
 
95
+ ```bash
96
+ pnpm build
97
+ ```
docs/backend-data-contract_en.md ADDED
@@ -0,0 +1,737 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Backend bucket data contract
2
+
3
+ This document defines the current data format in the private HF bucket that the frontend can
4
+ integrate with. It is a contract for JSON shapes, relationships between entities, and nullable
5
+ fields. Formula correctness, ranking logic, and business calculations remain the backend's
6
+ responsibility.
7
+
8
+ Verified snapshot:
9
+
10
+ ```json
11
+ {
12
+ "schema_version": "1.0.0",
13
+ "snapshot_id": "snapshot_2026-07-10",
14
+ "snapshot_date": "2026-07-10",
15
+ "generated_at": "2026-07-10T19:17:38+00:00",
16
+ "model_count": 48,
17
+ "group_count": 19,
18
+ "dataset_count": 44
19
+ }
20
+ ```
21
+
22
+ ## General rules
23
+
24
+ - All files use `schema_version: "1.0.0"`.
25
+ - All missing numeric metrics are represented as `null`; they are not omitted.
26
+ - `model_id`, `group_id`, and `dataset_id` are string identifiers.
27
+ - In the current bucket, `model_id` includes the run suffix, for example
28
+ `qwen_qwen3guard_gen_0_6b__run_20260606_005401_a4794dd1`.
29
+ - Display order comes from the `rows`, `model_ids`, `group_ids`, `row_ids`, and `column_ids`
30
+ arrays.
31
+ - `title`, `subtitle`, `x_axis`, `y_axis`, `label`, `description`, and similar translated
32
+ fields have the shape `{ "en": "...", "ru": "..." }`.
33
+ - `subtitle` may be present in visualization payloads; the frontend may use or ignore it.
34
+ - `manifest.files` is the source of paths to payload files.
35
+ - `manifest.hashes` contains SHA-256 hashes of payload files and must be updated whenever the
36
+ corresponding JSON changes.
37
+
38
+ ## File list
39
+
40
+ ```text
41
+ manifest.json
42
+ catalog.json
43
+ leaderboard.json
44
+ details_matrix.json
45
+ drilldown/index.json
46
+ visualizations/radar.json
47
+ visualizations/scatter.json
48
+ visualizations/heatmap.json
49
+ visualizations/grouped_bars.json
50
+ visualizations/pareto.json
51
+ visualizations/performance.json
52
+ visualizations/robustness.json
53
+ ```
54
+
55
+ ## Common types
56
+
57
+ ```ts
58
+ type LocalizedString = {
59
+ en: string;
60
+ ru: string;
61
+ };
62
+
63
+ type NullableNumber = number | null;
64
+ type MetricDirection = 'higher' | 'lower';
65
+ type MetricKey =
66
+ 'integral' | 'score' | 'f1' | 'recall' | 'precision' | 'accuracy' | 'fpr' | 'fnr' | 'latency_ms';
67
+
68
+ type LatencyMs = {
69
+ p50: NullableNumber;
70
+ p95: NullableNumber;
71
+ p99: NullableNumber;
72
+ };
73
+
74
+ type GroupMetrics = {
75
+ score: NullableNumber;
76
+ f1: NullableNumber;
77
+ fpr: NullableNumber;
78
+ fnr: NullableNumber;
79
+ };
80
+
81
+ type FullMetrics = GroupMetrics & {
82
+ recall: NullableNumber;
83
+ precision: NullableNumber;
84
+ accuracy: NullableNumber;
85
+ };
86
+ ```
87
+
88
+ ## manifest.json
89
+
90
+ ```ts
91
+ type BucketFileKey =
92
+ | 'catalog'
93
+ | 'leaderboard'
94
+ | 'details_matrix'
95
+ | 'drilldown_index'
96
+ | 'radar'
97
+ | 'scatter'
98
+ | 'heatmap'
99
+ | 'grouped_bars'
100
+ | 'pareto'
101
+ | 'performance'
102
+ | 'robustness';
103
+
104
+ type Manifest = {
105
+ schema_version: '1.0.0';
106
+ generated_at: string;
107
+ snapshot_id: string;
108
+ snapshot_date: string;
109
+ is_ok: boolean;
110
+ model_count: number;
111
+ group_count: number;
112
+ dataset_count: number;
113
+ files: Record<BucketFileKey, string>;
114
+ hashes: Record<BucketFileKey, string>;
115
+ };
116
+ ```
117
+
118
+ ```json
119
+ {
120
+ "schema_version": "1.0.0",
121
+ "generated_at": "2026-07-10T19:17:38+00:00",
122
+ "snapshot_id": "snapshot_2026-07-10",
123
+ "snapshot_date": "2026-07-10",
124
+ "is_ok": true,
125
+ "model_count": 48,
126
+ "group_count": 19,
127
+ "dataset_count": 44,
128
+ "files": {
129
+ "catalog": "catalog.json",
130
+ "leaderboard": "leaderboard.json",
131
+ "details_matrix": "details_matrix.json",
132
+ "drilldown_index": "drilldown/index.json",
133
+ "radar": "visualizations/radar.json",
134
+ "scatter": "visualizations/scatter.json",
135
+ "heatmap": "visualizations/heatmap.json",
136
+ "grouped_bars": "visualizations/grouped_bars.json",
137
+ "pareto": "visualizations/pareto.json",
138
+ "performance": "visualizations/performance.json",
139
+ "robustness": "visualizations/robustness.json"
140
+ },
141
+ "hashes": {
142
+ "catalog": "6cde39ef39563ec1db551620cdcbd6a8db4751b05a3d886c97453a382f0309fc",
143
+ "leaderboard": "3e1f957fd2e3d994b200b7d3b35f6606c3fc56fad6c1fe8facac72a513b88cc0",
144
+ "details_matrix": "ea9e89936cf59075e78c8f907b8a1b922f8ce1105a56669a829131e1ce56b4ba",
145
+ "drilldown_index": "5cc2ceca5eeb38514a872b91fe301b1481109404547bbcc9a30622eb19b1a4da",
146
+ "radar": "5ce3a62644f768feb3f52bd787993f6281eec935879d9fd04c10c3835028f249",
147
+ "scatter": "f8c5b6cc91a2f586fbfb9a69bcc9b1472363c4cdda61c6501aff9abcc14b0756",
148
+ "heatmap": "d2665f3f4df58ca2d05a138cee5186bf3a16d287d61c459e9b2ecc4e69914b1a",
149
+ "grouped_bars": "678a531e2b1593de9e8bdd45f17d116aa4f5051d4e246f2abdcf634d5d86d7f4",
150
+ "pareto": "0a1b2bad8addd2df35f86a90d4fce59a76f846d031b56278ef4fb029ff425535",
151
+ "performance": "5cecdcb010454e7918250c526eb69b20199ba8317017d9b30938104b97fb77bf",
152
+ "robustness": "362664c44b3acf2b5927de59fac7f8c3ed4492c6321351c21e68c139a250953f"
153
+ }
154
+ }
155
+ ```
156
+
157
+ ## catalog.json
158
+
159
+ ```ts
160
+ type BucketModel = {
161
+ model_id: string;
162
+ display_name: string;
163
+ short_name: string;
164
+ org: string;
165
+ family: string;
166
+ guardrail_type: 'llm-judge' | 'classifier' | 'unknown';
167
+ license: string | null;
168
+ params_b: NullableNumber;
169
+ size_label: LocalizedString | null;
170
+ languages: string[];
171
+ note: LocalizedString | null;
172
+ run_date: string;
173
+ hf_url: string | null;
174
+ eval_leak: boolean | null;
175
+ };
176
+
177
+ type BucketGroup = {
178
+ group_id: string;
179
+ label: LocalizedString;
180
+ description: LocalizedString | null;
181
+ what_it_tests: LocalizedString | null;
182
+ languages: string[];
183
+ dataset_ids: string[];
184
+ };
185
+
186
+ type BucketDataset = {
187
+ dataset_id: string;
188
+ group_id: string;
189
+ label: LocalizedString;
190
+ source_name: LocalizedString | null;
191
+ split: LocalizedString | null;
192
+ subset: LocalizedString | null;
193
+ language: string | null;
194
+ is_combined: boolean;
195
+ };
196
+
197
+ type Catalog = {
198
+ schema_version: '1.0.0';
199
+ models: BucketModel[];
200
+ groups: BucketGroup[];
201
+ datasets: BucketDataset[];
202
+ metric_directions: Record<MetricKey, MetricDirection>;
203
+ metric_labels: Partial<Record<MetricKey, LocalizedString>>;
204
+ };
205
+ ```
206
+
207
+ ```json
208
+ {
209
+ "schema_version": "1.0.0",
210
+ "models": [
211
+ {
212
+ "model_id": "qwen_qwen3guard_gen_0_6b__run_20260606_005401_a4794dd1",
213
+ "display_name": "Qwen3Guard-Gen-0.6B",
214
+ "short_name": "Qwen3Guard-Gen-0.6B",
215
+ "org": "Qwen",
216
+ "family": "Qwen3",
217
+ "guardrail_type": "llm-judge",
218
+ "license": "Apache-2.0",
219
+ "params_b": 0.6,
220
+ "size_label": { "en": "0.6B", "ru": "0.6B" },
221
+ "languages": ["multilingual", "en", "ru", "zh"],
222
+ "note": {
223
+ "en": "Small Qwen3Guard for low-latency; model_size marked as 0.8B in the run, actual checkpoint ~0.6B.",
224
+ "ru": "Малый Qwen3Guard для low-latency; model_size в прогоне помечен 0.8B, фактический чекпойнт ~0.6B."
225
+ },
226
+ "run_date": "2026-06-06",
227
+ "hf_url": "https://huggingface.co/Qwen/Qwen3Guard-Gen-0.6B",
228
+ "eval_leak": null
229
+ }
230
+ ],
231
+ "groups": [
232
+ {
233
+ "group_id": "s_eval",
234
+ "label": { "en": "S-Eval", "ru": "S-Eval" },
235
+ "description": {
236
+ "en": "S-Eval benchmark family.",
237
+ "ru": "Семейство бенчмарков S-Eval."
238
+ },
239
+ "what_it_tests": {
240
+ "en": "Recognition of unsafe intent and robustness to adversarial attacks.",
241
+ "ru": "Распознавание небезопасного намерения и устойчивость к adversarial-атакам."
242
+ },
243
+ "languages": ["en", "ru"],
244
+ "dataset_ids": ["s_eval_base_risk", "s_eval_attack_set"]
245
+ }
246
+ ],
247
+ "datasets": [
248
+ {
249
+ "dataset_id": "s_eval_base_risk",
250
+ "group_id": "s_eval",
251
+ "label": { "en": "S-Eval (base risk)", "ru": "S-Eval (base risk)" },
252
+ "source_name": null,
253
+ "split": null,
254
+ "subset": null,
255
+ "language": "en",
256
+ "is_combined": false
257
+ }
258
+ ],
259
+ "metric_directions": {
260
+ "integral": "higher",
261
+ "score": "higher",
262
+ "f1": "higher",
263
+ "recall": "higher",
264
+ "precision": "higher",
265
+ "accuracy": "higher",
266
+ "fpr": "lower",
267
+ "fnr": "lower",
268
+ "latency_ms": "lower"
269
+ },
270
+ "metric_labels": {
271
+ "score": { "en": "Score", "ru": "Скор" },
272
+ "f1": { "en": "F1", "ru": "F1" },
273
+ "fpr": { "en": "FPR", "ru": "FPR" },
274
+ "fnr": { "en": "FNR", "ru": "FNR" },
275
+ "integral": { "en": "Integral score", "ru": "Интегральный скор" },
276
+ "latency_ms": { "en": "Latency, ms", "ru": "Задержка, мс" }
277
+ }
278
+ }
279
+ ```
280
+
281
+ ## leaderboard.json
282
+
283
+ ```ts
284
+ type LeaderboardRow = {
285
+ model_id: string;
286
+ rank: number;
287
+ run_id: string;
288
+ run_date: string;
289
+ integral: NullableNumber;
290
+ min_group: NullableNumber;
291
+ overall_fpr: NullableNumber;
292
+ overall_fnr: NullableNumber;
293
+ overall_f1: NullableNumber;
294
+ latency_ms: LatencyMs;
295
+ groups: Record<string, GroupMetrics>;
296
+ };
297
+
298
+ type Leaderboard = {
299
+ schema_version: '1.0.0';
300
+ rows: LeaderboardRow[];
301
+ };
302
+ ```
303
+
304
+ ```json
305
+ {
306
+ "schema_version": "1.0.0",
307
+ "rows": [
308
+ {
309
+ "model_id": "alibaba_aaig_yufeng_xguard_reason_8b__run_20260701_155019_4a9b279f",
310
+ "rank": 1,
311
+ "run_id": "run_20260701_155019_4a9b279f",
312
+ "run_date": "2026-07-01",
313
+ "integral": 0.7605880393198141,
314
+ "min_group": 0.15654499428022556,
315
+ "overall_fpr": 0.21801750988077712,
316
+ "overall_fnr": 0.12210970263223303,
317
+ "overall_f1": 0.8224670573864103,
318
+ "latency_ms": { "p50": 71.79, "p95": 102.57, "p99": 144.35 },
319
+ "groups": {
320
+ "s_eval": {
321
+ "score": 0.8761476877122706,
322
+ "f1": null,
323
+ "fpr": null,
324
+ "fnr": 0.11835
325
+ }
326
+ }
327
+ }
328
+ ]
329
+ }
330
+ ```
331
+
332
+ ## details_matrix.json
333
+
334
+ ```ts
335
+ type GroupCell = FullMetrics & {
336
+ model_id: string;
337
+ group_id: string;
338
+ };
339
+
340
+ type DatasetCell = GroupCell & {
341
+ dataset_id: string;
342
+ sample_count: number | null;
343
+ };
344
+
345
+ type DetailsMatrix = {
346
+ schema_version: '1.0.0';
347
+ group_cells: GroupCell[];
348
+ dataset_cells: DatasetCell[];
349
+ };
350
+ ```
351
+
352
+ ```json
353
+ {
354
+ "schema_version": "1.0.0",
355
+ "group_cells": [
356
+ {
357
+ "model_id": "qwen_qwen3guard_gen_0_6b__run_20260606_005401_a4794dd1",
358
+ "group_id": "s_eval",
359
+ "score": 0.6505268251330913,
360
+ "f1": null,
361
+ "fpr": null,
362
+ "fnr": 0.34645000000000004,
363
+ "recall": null,
364
+ "precision": null,
365
+ "accuracy": null
366
+ }
367
+ ],
368
+ "dataset_cells": [
369
+ {
370
+ "model_id": "qwen_qwen3guard_gen_0_6b__run_20260606_005401_a4794dd1",
371
+ "group_id": "s_eval",
372
+ "dataset_id": "s_eval_base_risk",
373
+ "score": 0.698,
374
+ "f1": null,
375
+ "fpr": null,
376
+ "fnr": 0.302,
377
+ "recall": 0.698,
378
+ "precision": null,
379
+ "accuracy": null,
380
+ "sample_count": 1000
381
+ }
382
+ ]
383
+ }
384
+ ```
385
+
386
+ ## drilldown/index.json
387
+
388
+ ```ts
389
+ type DrilldownMetric = 'score' | 'f1' | 'fpr' | 'fnr';
390
+
391
+ type DrilldownHeatmapRow = {
392
+ model_id: string;
393
+ group_id: string;
394
+ dataset_ids: string[];
395
+ selected_metric: DrilldownMetric;
396
+ metrics: GroupMetrics;
397
+ description: LocalizedString | null;
398
+ prompt_viewer_available: boolean;
399
+ prompt_viewer_path: string | null;
400
+ };
401
+
402
+ type DrilldownIndex = {
403
+ schema_version: '1.0.0';
404
+ heatmap: DrilldownHeatmapRow[];
405
+ };
406
+ ```
407
+
408
+ ```json
409
+ {
410
+ "schema_version": "1.0.0",
411
+ "heatmap": [
412
+ {
413
+ "model_id": "qwen_qwen3guard_gen_0_6b__run_20260606_005401_a4794dd1",
414
+ "group_id": "s_eval",
415
+ "dataset_ids": ["s_eval_base_risk", "s_eval_attack_set"],
416
+ "selected_metric": "fnr",
417
+ "metrics": {
418
+ "score": 0.6505268251330913,
419
+ "f1": null,
420
+ "fpr": null,
421
+ "fnr": 0.34645000000000004
422
+ },
423
+ "description": {
424
+ "en": "Small Qwen3Guard for low-latency; model_size marked as 0.8B in the run, actual checkpoint ~0.6B.",
425
+ "ru": "Малый Qwen3Guard для low-latency; model_size в прогоне помечен 0.8B, фактический чекпойнт ~0.6B."
426
+ },
427
+ "prompt_viewer_available": false,
428
+ "prompt_viewer_path": null
429
+ }
430
+ ]
431
+ }
432
+ ```
433
+
434
+ ## visualizations/radar.json
435
+
436
+ ```ts
437
+ type Radar = {
438
+ schema_version: '1.0.0';
439
+ title: LocalizedString;
440
+ subtitle?: LocalizedString;
441
+ model_ids: string[];
442
+ group_ids: string[];
443
+ default_model_ids: string[];
444
+ values: Record<string, Record<string, NullableNumber>>;
445
+ f1_values: Record<string, Record<string, NullableNumber>>;
446
+ };
447
+ ```
448
+
449
+ `values` and `f1_values` are indexed as `values[group_id][model_id]`.
450
+
451
+ ```json
452
+ {
453
+ "schema_version": "1.0.0",
454
+ "title": { "en": "Model Safety Profile", "ru": "Профиль безопасности модели" },
455
+ "subtitle": { "en": "Group scores", "ru": "Групповые скоры" },
456
+ "model_ids": ["qwen_qwen3guard_gen_0_6b__run_20260606_005401_a4794dd1"],
457
+ "group_ids": ["s_eval"],
458
+ "default_model_ids": ["qwen_qwen3guard_gen_0_6b__run_20260606_005401_a4794dd1"],
459
+ "values": {
460
+ "s_eval": {
461
+ "qwen_qwen3guard_gen_0_6b__run_20260606_005401_a4794dd1": 0.6505268251330913
462
+ }
463
+ },
464
+ "f1_values": {
465
+ "s_eval": {
466
+ "qwen_qwen3guard_gen_0_6b__run_20260606_005401_a4794dd1": null
467
+ }
468
+ }
469
+ }
470
+ ```
471
+
472
+ ## visualizations/scatter.json
473
+
474
+ ```ts
475
+ type ScatterRowId = 'fpr' | 'fnr' | 'overall';
476
+
477
+ type ScatterPoint = {
478
+ model_id: string;
479
+ fpr: NullableNumber;
480
+ fnr: NullableNumber;
481
+ tooltip: string | null;
482
+ };
483
+
484
+ type Scatter = {
485
+ schema_version: '1.0.0';
486
+ title: LocalizedString;
487
+ x_axis: LocalizedString;
488
+ y_axis: LocalizedString;
489
+ points: ScatterPoint[];
490
+ row_ids: ScatterRowId[];
491
+ values: Record<ScatterRowId, Record<string, NullableNumber>>;
492
+ };
493
+ ```
494
+
495
+ `values` is indexed as `values[row_id][model_id]`.
496
+
497
+ ```json
498
+ {
499
+ "schema_version": "1.0.0",
500
+ "title": { "en": "FPR vs FNR (overall)", "ru": "FPR vs FNR (overall)" },
501
+ "x_axis": { "en": "FPR", "ru": "FPR" },
502
+ "y_axis": { "en": "FNR", "ru": "FNR" },
503
+ "points": [
504
+ {
505
+ "model_id": "alibaba_aaig_yufeng_xguard_reason_8b__run_20260701_155019_4a9b279f",
506
+ "fpr": 0.21801750988077712,
507
+ "fnr": 0.12210970263223303,
508
+ "tooltip": null
509
+ }
510
+ ],
511
+ "row_ids": ["fpr", "fnr", "overall"],
512
+ "values": {
513
+ "fpr": {
514
+ "alibaba_aaig_yufeng_xguard_reason_8b__run_20260701_155019_4a9b279f": 0.21801750988077712
515
+ },
516
+ "fnr": {
517
+ "alibaba_aaig_yufeng_xguard_reason_8b__run_20260701_155019_4a9b279f": 0.12210970263223303
518
+ },
519
+ "overall": {
520
+ "alibaba_aaig_yufeng_xguard_reason_8b__run_20260701_155019_4a9b279f": 0.7605880393198141
521
+ }
522
+ }
523
+ }
524
+ ```
525
+
526
+ ## visualizations/heatmap.json
527
+
528
+ ```ts
529
+ type Heatmap = {
530
+ schema_version: '1.0.0';
531
+ title: LocalizedString;
532
+ subtitle?: LocalizedString;
533
+ metric: 'score' | 'f1' | 'fpr' | 'fnr';
534
+ row_ids: string[];
535
+ column_ids: string[];
536
+ values: Record<string, Record<string, NullableNumber>>;
537
+ };
538
+ ```
539
+
540
+ `values` is indexed as `values[row_id][model_id]`. In the current snapshot, `metric` is
541
+ `"fnr"`.
542
+
543
+ ```json
544
+ {
545
+ "schema_version": "1.0.0",
546
+ "title": { "en": "Heatmap FNR", "ru": "Тепловая карта FNR" },
547
+ "subtitle": { "en": "Dataset x model", "ru": "Датасет x модель" },
548
+ "metric": "fnr",
549
+ "row_ids": ["aegis_2_0_prompt"],
550
+ "column_ids": ["qwen_qwen3guard_gen_0_6b__run_20260606_005401_a4794dd1"],
551
+ "values": {
552
+ "aegis_2_0_prompt": {
553
+ "qwen_qwen3guard_gen_0_6b__run_20260606_005401_a4794dd1": 0.05854579792256846
554
+ }
555
+ }
556
+ }
557
+ ```
558
+
559
+ ## visualizations/grouped_bars.json
560
+
561
+ ```ts
562
+ type GroupedBarsModel = {
563
+ model_id: string;
564
+ groups: Record<
565
+ string,
566
+ {
567
+ fnr: NullableNumber;
568
+ fpr: NullableNumber;
569
+ f1: NullableNumber;
570
+ }
571
+ >;
572
+ };
573
+
574
+ type GroupedBars = {
575
+ schema_version: '1.0.0';
576
+ title: LocalizedString;
577
+ subtitle?: LocalizedString;
578
+ group_ids: string[];
579
+ models: GroupedBarsModel[];
580
+ };
581
+ ```
582
+
583
+ ```json
584
+ {
585
+ "schema_version": "1.0.0",
586
+ "title": { "en": "Grouped bars", "ru": "Групповые столбцы" },
587
+ "subtitle": {
588
+ "en": "FNR / FPR / F1 by benchmark group",
589
+ "ru": "FNR / FPR / F1 по группам"
590
+ },
591
+ "group_ids": ["s_eval", "aegis_2_0"],
592
+ "models": [
593
+ {
594
+ "model_id": "qwen_qwen3guard_gen_0_6b__run_20260606_005401_a4794dd1",
595
+ "groups": {
596
+ "s_eval": { "fnr": 0.34645000000000004, "fpr": null, "f1": null },
597
+ "aegis_2_0": {
598
+ "fnr": 0.08384142688006596,
599
+ "fpr": 0.2825050061521388,
600
+ "f1": 0.8346634240315878
601
+ }
602
+ }
603
+ }
604
+ ]
605
+ }
606
+ ```
607
+
608
+ ## visualizations/pareto.json
609
+
610
+ ```ts
611
+ type ParetoModel = {
612
+ latency_ms: LatencyMs;
613
+ integral: NullableNumber;
614
+ fpr: NullableNumber;
615
+ };
616
+
617
+ type Pareto = {
618
+ schema_version: '1.0.0';
619
+ title: LocalizedString;
620
+ subtitle?: LocalizedString;
621
+ x_axis: LocalizedString;
622
+ y_axis: LocalizedString;
623
+ by_model_id: Record<string, ParetoModel>;
624
+ };
625
+ ```
626
+
627
+ ```json
628
+ {
629
+ "schema_version": "1.0.0",
630
+ "title": { "en": "Pareto: latency vs quality", "ru": "Парето: задержка vs качество" },
631
+ "subtitle": {
632
+ "en": "p95 latency vs integral score",
633
+ "ru": "p95 задержка vs интегральный скор"
634
+ },
635
+ "x_axis": { "en": "Latency p95, ms", "ru": "Задержка p95, мс" },
636
+ "y_axis": { "en": "Integral score", "ru": "Интегральный скор" },
637
+ "by_model_id": {
638
+ "alibaba_aaig_yufeng_xguard_reason_8b__run_20260701_155019_4a9b279f": {
639
+ "latency_ms": { "p50": 71.79, "p95": 102.57, "p99": 144.35 },
640
+ "integral": 0.7605880393198141,
641
+ "fpr": 0.21801750988077712
642
+ }
643
+ }
644
+ }
645
+ ```
646
+
647
+ ## visualizations/performance.json
648
+
649
+ ```ts
650
+ type PerformanceRow = {
651
+ model_id: string;
652
+ rank: number;
653
+ latency_ms: LatencyMs;
654
+ integral: NullableNumber;
655
+ };
656
+
657
+ type Performance = {
658
+ schema_version: '1.0.0';
659
+ title: LocalizedString;
660
+ rows: PerformanceRow[];
661
+ };
662
+ ```
663
+
664
+ ```json
665
+ {
666
+ "schema_version": "1.0.0",
667
+ "title": { "en": "Latency / performance", "ru": "Задержка / производительность" },
668
+ "rows": [
669
+ {
670
+ "model_id": "alibaba_aaig_yufeng_xguard_reason_8b__run_20260701_155019_4a9b279f",
671
+ "rank": 1,
672
+ "latency_ms": { "p50": 71.79, "p95": 102.57, "p99": 144.35 },
673
+ "integral": 0.7605880393198141
674
+ }
675
+ ]
676
+ }
677
+ ```
678
+
679
+ ## visualizations/robustness.json
680
+
681
+ ```ts
682
+ type RobustnessRow = {
683
+ model_id: string;
684
+ score_real: NullableNumber;
685
+ score_robust: NullableNumber;
686
+ delta_score: NullableNumber;
687
+ fnr_real: NullableNumber;
688
+ fnr_robust: NullableNumber;
689
+ delta_fnr: NullableNumber;
690
+ category_label: LocalizedString;
691
+ };
692
+
693
+ type Robustness = {
694
+ schema_version: '1.0.0';
695
+ title: LocalizedString;
696
+ rows: RobustnessRow[];
697
+ };
698
+ ```
699
+
700
+ ```json
701
+ {
702
+ "schema_version": "1.0.0",
703
+ "title": { "en": "Robustness comparison", "ru": "Сравнение robustness" },
704
+ "rows": [
705
+ {
706
+ "model_id": "qwen_qwen3guard_gen_0_6b__run_20260606_005401_a4794dd1",
707
+ "score_real": 0.6313559322033898,
708
+ "score_robust": 0.8121144139091419,
709
+ "delta_score": 0.18075848170575204,
710
+ "fnr_real": 0.13623188405797101,
711
+ "fnr_robust": 0.21190130624092887,
712
+ "delta_fnr": 0.07566942218295786,
713
+ "category_label": {
714
+ "en": "Qwen3Guard-Gen-0.6B",
715
+ "ru": "Qwen3Guard-Gen-0.6B"
716
+ }
717
+ }
718
+ ]
719
+ }
720
+ ```
721
+
722
+ ## Validation invariants
723
+
724
+ - `catalog.models.length === manifest.model_count`.
725
+ - `catalog.groups.length === manifest.group_count`.
726
+ - `catalog.datasets.length === manifest.dataset_count`.
727
+ - `leaderboard.rows[].model_id` must reference `catalog.models[].model_id`.
728
+ - `details_matrix.group_cells[].model_id` and `details_matrix.dataset_cells[].model_id` must
729
+ reference the catalog.
730
+ - `details_matrix.group_cells[].group_id` must reference `catalog.groups[].group_id`.
731
+ - `details_matrix.dataset_cells[].dataset_id` must reference `catalog.datasets[].dataset_id`.
732
+ - The current snapshot is expected to contain 48 unique `model_id` values in `catalog` and
733
+ `leaderboard`.
734
+ - The current snapshot is expected to contain 912 unique `model_id + group_id` pairs in
735
+ `details_matrix.group_cells`.
736
+ - The current snapshot is expected to contain 2112 unique `model_id + group_id + dataset_id`
737
+ triples in `details_matrix.dataset_cells`.
docs/{backend-data-contract.md → backend-data-contract_ru.md} RENAMED
File without changes
docs/huggingface-space-deploy_en.md ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Hugging Face Space deploy
2
+
3
+ Target deployment flow:
4
+
5
+ ```text
6
+ Corporate GitLab repository
7
+ ↓ GitLab CI
8
+ Private Hugging Face Docker Space
9
+ ↓ CPU Basic / Free
10
+ HiveTrace Leaderboard
11
+ ```
12
+
13
+ ## Hugging Face Space
14
+
15
+ The Space is created manually once.
16
+
17
+ Recommended settings:
18
+
19
+ - Visibility: `Private`
20
+ - SDK: `Docker`
21
+ - Hardware: `CPU Basic` / free
22
+ - App port: `5173`
23
+
24
+ The project `README.md` already contains the Space frontmatter:
25
+
26
+ ```yaml
27
+ ---
28
+ title: HiveTrace Leaderboard
29
+ emoji: 🛡️
30
+ colorFrom: green
31
+ colorTo: indigo
32
+ sdk: docker
33
+ app_port: 5173
34
+ pinned: true
35
+ ---
36
+ ```
37
+
38
+ ## Runtime secrets and variables in Hugging Face
39
+
40
+ Add the runtime secret in Space Settings:
41
+
42
+ ```text
43
+ HF_TOKEN=hf_...
44
+ ```
45
+
46
+ This token is required by the app to read the private Hugging Face bucket. Do not expose it as
47
+ a `PUBLIC_*` variable and do not commit it to the repository.
48
+
49
+ Variables:
50
+
51
+ ```text
52
+ HF_BUCKET_ID=hivetrace/leaderboard_frontend_v2
53
+ HF_BUCKET_PREFIX=latest
54
+ HF_BUCKET_CACHE_TTL_MS=300000
55
+ HF_BUCKET_REQUEST_TIMEOUT_MS=10000
56
+ ```
57
+
58
+ Optional:
59
+
60
+ ```text
61
+ HF_BUCKET_ENDPOINT=https://huggingface.co
62
+ ```
63
+
64
+ ## GitLab CI variables
65
+
66
+ Add the following CI/CD variables in the corporate GitLab project:
67
+
68
+ ```text
69
+ HF_USERNAME=<huggingface-user-or-service-account-name>
70
+ HF_SPACE_TOKEN=<huggingface-write-token-for-space-repo>
71
+ HF_SPACE_REPO=<namespace>/<space-name>
72
+ ```
73
+
74
+ Example:
75
+
76
+ ```text
77
+ HF_SPACE_REPO=hivetrace/guard-leaderboard
78
+ ```
79
+
80
+ `HF_SPACE_TOKEN` must have write access to the Space repository.
81
+
82
+ Important: keep `HF_SPACE_TOKEN` and the runtime `HF_TOKEN` used for bucket reads as separate
83
+ tokens.
84
+
85
+ In GitLab, mark `HF_SPACE_TOKEN` as a masked/protected variable.
86
+
87
+ ## Deploy flow
88
+
89
+ `.gitlab-ci.yml` runs two stages:
90
+
91
+ 1. `validate`
92
+ - `pnpm install --frozen-lockfile`
93
+ - `pnpm check` (generates Paraglide output internally)
94
+ - `pnpm build`
95
+
96
+ 2. `deploy:huggingface`
97
+ - runs only on the `prod` branch
98
+ - pushes the current commit to the private Hugging Face Space repository
99
+ - uses force push because the Hugging Face Space repository is a deploy repository
100
+ - the Space automatically rebuilds the Docker image after the push
101
+
102
+ ## Production branch
103
+
104
+ Production deployment runs only from the `prod` branch.
105
+
106
+ Typical flow:
107
+
108
+ ```bash
109
+ git checkout prod
110
+ git merge main
111
+ git push origin prod
112
+ ```
113
+
114
+ After the push to `prod`, GitLab CI runs validation and then sends this commit to the `main`
115
+ branch of the Hugging Face Space repository.
116
+
117
+ ## Local Docker smoke test
118
+
119
+ Before the first deployment, it is useful to test the Docker image locally:
120
+
121
+ ```bash
122
+ docker build -t hivetrace-leaderboard .
123
+ docker run --rm -p 5173:5173 \
124
+ -e HF_TOKEN=hf_... \
125
+ -e HF_BUCKET_ID=hivetrace/leaderboard_frontend_v2 \
126
+ -e HF_BUCKET_PREFIX=latest \
127
+ hivetrace-leaderboard
128
+ ```
129
+
130
+ After startup, open:
131
+
132
+ ```text
133
+ http://127.0.0.1:5173/
134
+ ```
docs/{huggingface-space-deploy.md → huggingface-space-deploy_ru.md} RENAMED
File without changes
package.json CHANGED
@@ -8,7 +8,7 @@
8
  "dev": "vite dev",
9
  "build": "vite build",
10
  "preview": "vite preview",
11
- "paraglide": "paraglide-js compile --project ./project.inlang --outdir ./src/lib/paraglide",
12
  "prepare": "svelte-kit sync || echo ''",
13
  "check": "pnpm run paraglide && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
14
  "check:watch": "pnpm run paraglide && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
 
8
  "dev": "vite dev",
9
  "build": "vite build",
10
  "preview": "vite preview",
11
+ "paraglide": "paraglide-js compile --project ./project.inlang --outdir ./src/lib/paraglide --strategy url baseLocale",
12
  "prepare": "svelte-kit sync || echo ''",
13
  "check": "pnpm run paraglide && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
14
  "check:watch": "pnpm run paraglide && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
pnpm-workspace.yaml CHANGED
@@ -1,3 +1,6 @@
 
 
 
1
  allowBuilds:
2
  '@tailwindcss/oxide': true
3
  esbuild: true
 
1
+ engineStrict: true
2
+ pmOnFail: warn
3
+ verifyDepsBeforeRun: warn
4
  allowBuilds:
5
  '@tailwindcss/oxide': true
6
  esbuild: true
src/lib/components/BucketDataFallback.svelte CHANGED
@@ -4,6 +4,7 @@
4
  import Empty from '$lib/components/Empty.svelte';
5
  import { Button } from '$lib/components/ui/button';
6
  import { m } from '$lib/paraglide/messages';
 
7
  </script>
8
 
9
  <div
@@ -26,7 +27,7 @@
26
  if (browser) location.reload();
27
  }}>{m.reload_page()}</Button
28
  >
29
- <Button variant="secondary" href="/">{m.to_main_page()}</Button>
30
  </div>
31
  </div>
32
  {/snippet}
 
4
  import Empty from '$lib/components/Empty.svelte';
5
  import { Button } from '$lib/components/ui/button';
6
  import { m } from '$lib/paraglide/messages';
7
+ import { localizeHref } from '$lib/paraglide/runtime';
8
  </script>
9
 
10
  <div
 
27
  if (browser) location.reload();
28
  }}>{m.reload_page()}</Button
29
  >
30
+ <Button variant="secondary" href={localizeHref('/')}>{m.to_main_page()}</Button>
31
  </div>
32
  </div>
33
  {/snippet}
src/lib/components/Navigation.svelte CHANGED
@@ -1,10 +1,12 @@
1
  <script lang="ts">
2
  import { resolve } from '$app/paths';
3
  import { page } from '$app/state';
 
4
  import Logo from '$lib/components/logo/logo.svelte';
5
  import * as NavigationMenu from '$lib/components/ui/navigation-menu/index.js';
6
  import { navigationMenuTriggerStyle } from '$lib/components/ui/navigation-menu/navigation-menu-trigger.svelte';
7
  import { m } from '$lib/paraglide/messages.js';
 
8
  import { cn } from '$lib/utils.js';
9
 
10
  const navItems = [
@@ -19,15 +21,17 @@
19
  'bg-[linear-gradient(135deg,#0019ff_0%,#3a55ff_45%,#6b4bf5_100%)] text-white shadow-sm shadow-[#0019ff]/20 hover:text-white hover:opacity-95 focus:text-white';
20
  const inactiveNavLinkClass = 'text-muted-foreground hover:text-foreground focus:text-foreground';
21
 
 
 
22
  const isActive = (href: string) => {
23
- if (href === '/') return page.url.pathname === '/';
24
 
25
- return page.url.pathname.startsWith(href);
26
  };
27
  </script>
28
 
29
  <a
30
- href={resolve('/')}
31
  class="flex min-w-0 shrink-0 items-center gap-3 rounded-md focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:outline-none"
32
  aria-label={m.brand_home_aria_label()}
33
  >
@@ -49,7 +53,7 @@
49
  <NavigationMenu.Link active={isActive(item.href)}>
50
  {#snippet child()}
51
  <a
52
- href={resolve(item.href)}
53
  aria-current={isActive(item.href) ? 'page' : undefined}
54
  class={cn(
55
  navigationMenuTriggerStyle(),
 
1
  <script lang="ts">
2
  import { resolve } from '$app/paths';
3
  import { page } from '$app/state';
4
+ import type { PathnameWithSearchOrHash } from '$app/types';
5
  import Logo from '$lib/components/logo/logo.svelte';
6
  import * as NavigationMenu from '$lib/components/ui/navigation-menu/index.js';
7
  import { navigationMenuTriggerStyle } from '$lib/components/ui/navigation-menu/navigation-menu-trigger.svelte';
8
  import { m } from '$lib/paraglide/messages.js';
9
+ import { deLocalizeHref, localizeHref } from '$lib/paraglide/runtime';
10
  import { cn } from '$lib/utils.js';
11
 
12
  const navItems = [
 
21
  'bg-[linear-gradient(135deg,#0019ff_0%,#3a55ff_45%,#6b4bf5_100%)] text-white shadow-sm shadow-[#0019ff]/20 hover:text-white hover:opacity-95 focus:text-white';
22
  const inactiveNavLinkClass = 'text-muted-foreground hover:text-foreground focus:text-foreground';
23
 
24
+ const currentPath = $derived(deLocalizeHref(page.url.pathname));
25
+
26
  const isActive = (href: string) => {
27
+ if (href === '/') return currentPath === '/';
28
 
29
+ return currentPath.startsWith(href);
30
  };
31
  </script>
32
 
33
  <a
34
+ href={resolve(localizeHref('/') as PathnameWithSearchOrHash)}
35
  class="flex min-w-0 shrink-0 items-center gap-3 rounded-md focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:outline-none"
36
  aria-label={m.brand_home_aria_label()}
37
  >
 
53
  <NavigationMenu.Link active={isActive(item.href)}>
54
  {#snippet child()}
55
  <a
56
+ href={resolve(localizeHref(item.href) as PathnameWithSearchOrHash)}
57
  aria-current={isActive(item.href) ? 'page' : undefined}
58
  class={cn(
59
  navigationMenuTriggerStyle(),
src/lib/components/errors/Error404.svelte CHANGED
@@ -3,6 +3,7 @@
3
  import Empty from '$lib/components/Empty.svelte';
4
  import { Button } from '$lib/components/ui/button';
5
  import { m } from '$lib/paraglide/messages';
 
6
  import { FolderCodeIcon } from '@lucide/svelte';
7
  </script>
8
 
@@ -18,7 +19,7 @@
18
  }}
19
  >{m.go_back()}
20
  </Button>
21
- <Button variant="secondary" href="/">{m.to_main_page()}</Button>
22
  </div>
23
  {/snippet}
24
  </Empty>
 
3
  import Empty from '$lib/components/Empty.svelte';
4
  import { Button } from '$lib/components/ui/button';
5
  import { m } from '$lib/paraglide/messages';
6
+ import { localizeHref } from '$lib/paraglide/runtime';
7
  import { FolderCodeIcon } from '@lucide/svelte';
8
  </script>
9
 
 
19
  }}
20
  >{m.go_back()}
21
  </Button>
22
+ <Button variant="secondary" href={localizeHref('/')}>{m.to_main_page()}</Button>
23
  </div>
24
  {/snippet}
25
  </Empty>
src/lib/components/errors/Error500.svelte CHANGED
@@ -3,6 +3,7 @@
3
  import { Button } from '$lib/components/ui/button';
4
  import Empty from '$lib/components/Empty.svelte';
5
  import { m } from '$lib/paraglide/messages';
 
6
  import { ServerCrash } from '@lucide/svelte';
7
  </script>
8
 
@@ -21,7 +22,7 @@
21
  if (browser) location.reload();
22
  }}>{m.reload_page()}</Button
23
  >
24
- <Button variant="secondary" href="/">{m.to_main_page()}</Button>
25
  </div>
26
  {/snippet}
27
  </Empty>
 
3
  import { Button } from '$lib/components/ui/button';
4
  import Empty from '$lib/components/Empty.svelte';
5
  import { m } from '$lib/paraglide/messages';
6
+ import { localizeHref } from '$lib/paraglide/runtime';
7
  import { ServerCrash } from '@lucide/svelte';
8
  </script>
9
 
 
22
  if (browser) location.reload();
23
  }}>{m.reload_page()}</Button
24
  >
25
+ <Button variant="secondary" href={localizeHref('/')}>{m.to_main_page()}</Button>
26
  </div>
27
  {/snippet}
28
  </Empty>
src/lib/server/hf-bucket/details-cache.ts CHANGED
@@ -5,10 +5,11 @@ import { adaptBucketDetails } from './adapt-details';
5
  import { fetchBucketDetailsSnapshot } from './details-snapshot';
6
  import { emptyDetailsDataset } from './fallback-data';
7
  import { toPublicBucketError } from './public-error';
 
8
 
9
  const DEFAULT_CACHE_TTL_MS = 5 * 60 * 1000;
10
 
11
- let cached: { value: DetailsDataset; expiresAt: number } | undefined;
12
  let inFlight: Promise<DetailsDataset> | undefined;
13
 
14
  function cacheTtlMs() {
@@ -17,8 +18,18 @@ function cacheTtlMs() {
17
  }
18
 
19
  async function refreshDetails() {
20
- const value = adaptBucketDetails(await fetchBucketDetailsSnapshot());
21
- cached = { value, expiresAt: Date.now() + cacheTtlMs() };
 
 
 
 
 
 
 
 
 
 
22
  return value;
23
  }
24
 
 
5
  import { fetchBucketDetailsSnapshot } from './details-snapshot';
6
  import { emptyDetailsDataset } from './fallback-data';
7
  import { toPublicBucketError } from './public-error';
8
+ import { fetchBucketRankingSnapshot } from './ranking-snapshot';
9
 
10
  const DEFAULT_CACHE_TTL_MS = 5 * 60 * 1000;
11
 
12
+ let cached: { value: DetailsDataset; snapshotId: string; expiresAt: number } | undefined;
13
  let inFlight: Promise<DetailsDataset> | undefined;
14
 
15
  function cacheTtlMs() {
 
18
  }
19
 
20
  async function refreshDetails() {
21
+ const rankingSnapshot = await fetchBucketRankingSnapshot();
22
+ if (cached?.snapshotId === rankingSnapshot.manifest.snapshot_id) {
23
+ cached.expiresAt = Date.now() + cacheTtlMs();
24
+ return cached.value;
25
+ }
26
+
27
+ const value = adaptBucketDetails(await fetchBucketDetailsSnapshot(rankingSnapshot));
28
+ cached = {
29
+ value,
30
+ snapshotId: rankingSnapshot.manifest.snapshot_id,
31
+ expiresAt: Date.now() + cacheTtlMs()
32
+ };
33
  return value;
34
  }
35
 
src/lib/server/hf-bucket/details-snapshot.ts CHANGED
@@ -1,7 +1,7 @@
1
  import { fetchHfBucketText, HfBucketError } from './client';
2
  import { parseBucketDetailsMatrix } from './details-payload';
3
  import { fetchBucketRankingSnapshot, verifyBucketFileHash } from './ranking-snapshot';
4
- import type { BucketDetailsSnapshot } from './types';
5
 
6
  function parseJson(text: string, path: string) {
7
  try {
@@ -68,8 +68,10 @@ function verifyDetailsSnapshot(snapshot: BucketDetailsSnapshot) {
68
  }
69
  }
70
 
71
- export async function fetchBucketDetailsSnapshot(): Promise<BucketDetailsSnapshot> {
72
- const rankingSnapshot = await fetchBucketRankingSnapshot();
 
 
73
  const path = rankingSnapshot.manifest.files.details_matrix;
74
  const text = await fetchHfBucketText(path);
75
  verifyBucketFileHash(rankingSnapshot.manifest, 'details_matrix', text);
 
1
  import { fetchHfBucketText, HfBucketError } from './client';
2
  import { parseBucketDetailsMatrix } from './details-payload';
3
  import { fetchBucketRankingSnapshot, verifyBucketFileHash } from './ranking-snapshot';
4
+ import type { BucketDetailsSnapshot, BucketRankingSnapshot } from './types';
5
 
6
  function parseJson(text: string, path: string) {
7
  try {
 
68
  }
69
  }
70
 
71
+ export async function fetchBucketDetailsSnapshot(
72
+ rankingSnapshot?: BucketRankingSnapshot
73
+ ): Promise<BucketDetailsSnapshot> {
74
+ rankingSnapshot ??= await fetchBucketRankingSnapshot();
75
  const path = rankingSnapshot.manifest.files.details_matrix;
76
  const text = await fetchHfBucketText(path);
77
  verifyBucketFileHash(rankingSnapshot.manifest, 'details_matrix', text);
src/lib/server/hf-bucket/ranking-cache.ts CHANGED
@@ -8,7 +8,7 @@ import { fetchBucketRankingSnapshot } from './ranking-snapshot';
8
 
9
  const DEFAULT_CACHE_TTL_MS = 5 * 60 * 1000;
10
 
11
- let cached: { value: RankingDataset; expiresAt: number } | undefined;
12
  let inFlight: Promise<RankingDataset> | undefined;
13
 
14
  function cacheTtlMs() {
@@ -17,8 +17,18 @@ function cacheTtlMs() {
17
  }
18
 
19
  async function refreshRanking() {
20
- const value = adaptBucketRanking(await fetchBucketRankingSnapshot());
21
- cached = { value, expiresAt: Date.now() + cacheTtlMs() };
 
 
 
 
 
 
 
 
 
 
22
  return value;
23
  }
24
 
 
8
 
9
  const DEFAULT_CACHE_TTL_MS = 5 * 60 * 1000;
10
 
11
+ let cached: { value: RankingDataset; snapshotId: string; expiresAt: number } | undefined;
12
  let inFlight: Promise<RankingDataset> | undefined;
13
 
14
  function cacheTtlMs() {
 
17
  }
18
 
19
  async function refreshRanking() {
20
+ const snapshot = await fetchBucketRankingSnapshot();
21
+ if (cached?.snapshotId === snapshot.manifest.snapshot_id) {
22
+ cached.expiresAt = Date.now() + cacheTtlMs();
23
+ return cached.value;
24
+ }
25
+
26
+ const value = adaptBucketRanking(snapshot);
27
+ cached = {
28
+ value,
29
+ snapshotId: snapshot.manifest.snapshot_id,
30
+ expiresAt: Date.now() + cacheTtlMs()
31
+ };
32
  return value;
33
  }
34
 
src/lib/server/hf-bucket/ranking-snapshot.ts CHANGED
@@ -89,8 +89,12 @@ function verifySnapshot(snapshot: BucketRankingSnapshot) {
89
  }
90
  }
91
 
92
- export async function fetchBucketRankingSnapshot(): Promise<BucketRankingSnapshot> {
93
- const { manifest } = await getHfBucketManifest({ forceRefresh: true });
 
 
 
 
94
  const catalogPath = manifest.files.catalog;
95
  const leaderboardPath = manifest.files.leaderboard;
96
  const [catalogText, leaderboardText] = await Promise.all([
@@ -116,3 +120,26 @@ export async function fetchBucketRankingSnapshot(): Promise<BucketRankingSnapsho
116
  verifySnapshot(snapshot);
117
  return snapshot;
118
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  }
90
  }
91
 
92
+ let cachedSnapshot: BucketRankingSnapshot | undefined;
93
+ let inFlight: Promise<BucketRankingSnapshot> | undefined;
94
+
95
+ async function fetchRemoteRankingSnapshot(
96
+ manifest: HfBucketManifest
97
+ ): Promise<BucketRankingSnapshot> {
98
  const catalogPath = manifest.files.catalog;
99
  const leaderboardPath = manifest.files.leaderboard;
100
  const [catalogText, leaderboardText] = await Promise.all([
 
120
  verifySnapshot(snapshot);
121
  return snapshot;
122
  }
123
+
124
+ export async function fetchBucketRankingSnapshot(options?: {
125
+ forceRefresh?: boolean;
126
+ }): Promise<BucketRankingSnapshot> {
127
+ const { manifest } = await getHfBucketManifest({ forceRefresh: options?.forceRefresh });
128
+
129
+ if (!options?.forceRefresh && cachedSnapshot?.manifest.snapshot_id === manifest.snapshot_id) {
130
+ return cachedSnapshot;
131
+ }
132
+
133
+ if (!inFlight) {
134
+ inFlight = fetchRemoteRankingSnapshot(manifest)
135
+ .then((snapshot) => {
136
+ cachedSnapshot = snapshot;
137
+ return snapshot;
138
+ })
139
+ .finally(() => {
140
+ inFlight = undefined;
141
+ });
142
+ }
143
+
144
+ return inFlight;
145
+ }
src/lib/server/hf-bucket/tools-cache.ts CHANGED
@@ -4,12 +4,15 @@ import type { ToolsDataset } from '$lib/types/tools-data';
4
  import { adaptBucketTools } from './adapt-tools';
5
  import { emptyToolsDataset } from './fallback-data';
6
  import { toPublicBucketError } from './public-error';
 
7
  import { fetchBucketVisualizationSnapshot } from './tools-snapshot';
8
  import type { BucketVisualizationSnapshot } from './types';
9
 
10
  const DEFAULT_CACHE_TTL_MS = 5 * 60 * 1000;
11
 
12
- let cached: { value: BucketVisualizationSnapshot; expiresAt: number } | undefined;
 
 
13
  let inFlight: Promise<BucketVisualizationSnapshot> | undefined;
14
 
15
  function cacheTtlMs() {
@@ -18,8 +21,18 @@ function cacheTtlMs() {
18
  }
19
 
20
  async function refreshToolsSnapshot() {
21
- const value = await fetchBucketVisualizationSnapshot();
22
- cached = { value, expiresAt: Date.now() + cacheTtlMs() };
 
 
 
 
 
 
 
 
 
 
23
  return value;
24
  }
25
 
@@ -110,7 +123,13 @@ export async function getHfBucketToolsState(): Promise<BucketDataState<ToolsData
110
  }
111
 
112
  try {
113
- const data = adaptBucketTools(snapshotState.data);
 
 
 
 
 
 
114
  if (snapshotState.status === 'stale') {
115
  return {
116
  status: 'stale',
 
4
  import { adaptBucketTools } from './adapt-tools';
5
  import { emptyToolsDataset } from './fallback-data';
6
  import { toPublicBucketError } from './public-error';
7
+ import { fetchBucketRankingSnapshot } from './ranking-snapshot';
8
  import { fetchBucketVisualizationSnapshot } from './tools-snapshot';
9
  import type { BucketVisualizationSnapshot } from './types';
10
 
11
  const DEFAULT_CACHE_TTL_MS = 5 * 60 * 1000;
12
 
13
+ let cached:
14
+ { value: BucketVisualizationSnapshot; snapshotId: string; expiresAt: number } | undefined;
15
+ let adaptedCached: { value: ToolsDataset; snapshotId: string } | undefined;
16
  let inFlight: Promise<BucketVisualizationSnapshot> | undefined;
17
 
18
  function cacheTtlMs() {
 
21
  }
22
 
23
  async function refreshToolsSnapshot() {
24
+ const rankingSnapshot = await fetchBucketRankingSnapshot();
25
+ if (cached?.snapshotId === rankingSnapshot.manifest.snapshot_id) {
26
+ cached.expiresAt = Date.now() + cacheTtlMs();
27
+ return cached.value;
28
+ }
29
+
30
+ const value = await fetchBucketVisualizationSnapshot(rankingSnapshot);
31
+ cached = {
32
+ value,
33
+ snapshotId: rankingSnapshot.manifest.snapshot_id,
34
+ expiresAt: Date.now() + cacheTtlMs()
35
+ };
36
  return value;
37
  }
38
 
 
123
  }
124
 
125
  try {
126
+ const snapshotId = snapshotState.data.manifest.snapshot_id;
127
+ const data =
128
+ adaptedCached?.snapshotId === snapshotId
129
+ ? adaptedCached.value
130
+ : adaptBucketTools(snapshotState.data);
131
+ adaptedCached = { value: data, snapshotId };
132
+
133
  if (snapshotState.status === 'stale') {
134
  return {
135
  status: 'stale',
src/lib/server/hf-bucket/tools-snapshot.ts CHANGED
@@ -13,6 +13,7 @@ import {
13
  import type {
14
  BucketMetricMap,
15
  BucketVisualizationPayloadByKey,
 
16
  BucketVisualizationSnapshot
17
  } from './types';
18
 
@@ -121,8 +122,10 @@ async function fetchVisualizationText(
121
  return { path, text };
122
  }
123
 
124
- export async function fetchBucketVisualizationSnapshot(): Promise<BucketVisualizationSnapshot> {
125
- const rankingSnapshot = await fetchBucketRankingSnapshot();
 
 
126
  const [
127
  drilldownIndexText,
128
  radarText,
 
13
  import type {
14
  BucketMetricMap,
15
  BucketVisualizationPayloadByKey,
16
+ BucketRankingSnapshot,
17
  BucketVisualizationSnapshot
18
  } from './types';
19
 
 
122
  return { path, text };
123
  }
124
 
125
+ export async function fetchBucketVisualizationSnapshot(
126
+ rankingSnapshot?: BucketRankingSnapshot
127
+ ): Promise<BucketVisualizationSnapshot> {
128
+ rankingSnapshot ??= await fetchBucketRankingSnapshot();
129
  const [
130
  drilldownIndexText,
131
  radarText,
vite.config.ts CHANGED
@@ -15,6 +15,10 @@ export default defineConfig({
15
  },
16
  adapter: adapter()
17
  }),
18
- paraglideVitePlugin({ project: './project.inlang', outdir: './src/lib/paraglide' })
 
 
 
 
19
  ]
20
  });
 
15
  },
16
  adapter: adapter()
17
  }),
18
+ paraglideVitePlugin({
19
+ project: './project.inlang',
20
+ outdir: './src/lib/paraglide',
21
+ strategy: ['url', 'baseLocale']
22
+ })
23
  ]
24
  });