vimp-engine 0.38.2 → 0.38.3

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 (56) hide show
  1. package/core/Cargo.toml +1 -1
  2. package/package.json +3 -1
  3. package/src/client/boot.js +30 -7
  4. package/src/client/components/controller/ClientReports.js +1 -1
  5. package/src/client/components/controller/Games.js +1 -1
  6. package/src/client/components/model/ClientReports.js +1 -1
  7. package/src/client/components/model/Games.js +24 -0
  8. package/src/client/components/view/ClientReports.js +1 -1
  9. package/src/client/components/view/Games.js +19 -1
  10. package/src/client/components/view/Lobby.js +1 -7
  11. package/src/client/debug.js +1 -1
  12. package/src/client/lib/authApi.js +1 -1
  13. package/src/client/lib/diagnostics.js +19 -4
  14. package/src/client/lib/engineVersion.js +0 -6
  15. package/src/client/lib/footerLink.js +15 -14
  16. package/src/client/lib/retryOnNetworkError.js +60 -0
  17. package/src/client/main.js +35 -18
  18. package/src/client/network/HiddenHostHealthLog.js +40 -3
  19. package/src/client/network/HostController.js +4 -4
  20. package/src/client/network/HostHealthPolicy.js +41 -0
  21. package/src/client/network/InlineHostBridge.js +8 -1
  22. package/src/client/session/HandoffFlow.js +5 -0
  23. package/src/client/session/HostRole.js +5 -2
  24. package/src/client/style.css +5 -6
  25. package/src/client/views/gameShell.js +3 -3
  26. package/src/client/views/includes/auth.pug +4 -4
  27. package/src/client/views/includes/lobby.pug +4 -4
  28. package/src/client/views/includes/reports.pug +1 -1
  29. package/src/config/authClient.js +3 -3
  30. package/src/config/lobby.js +4 -5
  31. package/src/config/opcodes.js +2 -1
  32. package/src/devtools/contract/loadContext.js +2 -2
  33. package/src/devtools/contract/rules/a7-package-repository.js +7 -8
  34. package/src/devtools/contract/rules/index.js +1 -1
  35. package/src/devtools/invariants.js +1 -1
  36. package/src/host/DebugRecorder.js +1 -1
  37. package/src/host/HostGame.js +35 -15
  38. package/src/host/commandFailure.js +140 -0
  39. package/src/host/host.worker.js +8 -2
  40. package/src/host/meta/SocketManager.js +2 -2
  41. package/src/host/meta/core/CommandProcessor.js +37 -7
  42. package/src/lib/applyRoomOverrides.js +1 -1
  43. package/src/lib/capabilities.js +1 -1
  44. package/src/lib/checkpointCodec.js +1 -2
  45. package/src/lib/coreConfig.js +1 -1
  46. package/src/lib/createHostRuntime.js +1 -1
  47. package/src/lib/gameId.js +1 -1
  48. package/src/lib/gamePlugin.js +6 -2
  49. package/src/lib/jwt.js +1 -1
  50. package/src/lib/packageLink.js +18 -61
  51. package/src/lib/validators.js +3 -3
  52. package/src/standalone/index.js +1 -1
  53. package/tests/fixtures/generations/gen-api3/README.md +2 -2
  54. package/tests/fixtures/generations/gen-api4/README.md +2 -2
  55. package/src/devtools/surface/abiParse.js +0 -175
  56. package/src/devtools/surface/collect.js +0 -413
@@ -0,0 +1,140 @@
1
+ // Отчёт о сбое чат-команды игры для приёмника диагностики
2
+ // (HostGame.setDiagnosticSink): сообщение в одну строку и стек без текста
3
+ // ошибки. Один отчёт на все среды — Worker (журнал платформы), dedicated
4
+ // (лог процесса), standalone (консоль вкладки). Чистые функции, без DOM и
5
+ // Node API: модуль работает в Worker'е
6
+
7
+ // символы, подделывающие строки лога: управляющие (перевод строки, ESC),
8
+ // невидимые форматные (bidi-переопределения, нулевая ширина, BOM), разделители
9
+ // строк и абзацев (U+2028/2029)
10
+ const LOG_UNSAFE = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}\u180E]/gu;
11
+ const MAX_STACK = 2000;
12
+
13
+ // обрезка до limit единиц UTF-16 без разрыва суррогатной пары; выполняется
14
+ // до очистки, чтобы огромная строка не проходила regex целиком
15
+ const clip = (value, limit) => {
16
+ if (value.length <= limit) {
17
+ return value;
18
+ }
19
+
20
+ const text = value.slice(0, limit);
21
+
22
+ return /[\uD800-\uDBFF]$/.test(text) ? text.slice(0, -1) : text;
23
+ };
24
+
25
+ // одна строка лога: текст от игрока или из кода игры без LOG_UNSAFE
26
+ const oneLine = (value, limit) =>
27
+ clip(String(value), limit).replace(LOG_UNSAFE, ' ');
28
+
29
+ // стек сохраняет переводы строк, остальное LOG_UNSAFE заменяется; длина
30
+ // ограничена
31
+ const cleanStack = stack =>
32
+ clip(stack, MAX_STACK)
33
+ .split(/\r\n|\r|\n/)
34
+ .map(line => line.replace(LOG_UNSAFE, ' '))
35
+ .join('\n');
36
+
37
+ // кадры стека (V8: « at …»). Заголовок «имя: текст» снимается по числу
38
+ // строк текста — ввод игрока в тексте не отличить от кадра по виду, поиск
39
+ // «at» его бы не отсёк. Не сошлось (текст изменён после создания, цепочка
40
+ // cause, другой движок) — остаётся хвост из подряд идущих кадров; ни кадров
41
+ // V8, ни кадров SpiderMonkey/JSC — стек сводится к одной строке, чтобы текст
42
+ // из него не подделал строки лога
43
+ const V8_FRAME = /^ {4}at /;
44
+
45
+ // кадр SpiderMonkey/JavaScriptCore: «имя@место», текста ошибки в таком стеке
46
+ // нет. Имя — без пробелов, кроме служебных имён JSC (global/eval/module code,
47
+ // «async имя», у анонимной функции — «async »): заголовок V8 «имя: текст» за
48
+ // кадр не сойдёт. Место — URL со схемой, [native code] или кадр Wasm у JSC:
49
+ // wasm-function[N] с именем модуля или без, у WebKit до 2025-08 — [wasm code]
50
+ // (имя тогда «модуль.wasm-function[N]»). Голый [native code] — тоже кадр
51
+ const FRAME_NAME = [
52
+ String.raw`[^\s@]*`,
53
+ '(?:global|eval|module) code',
54
+ String.raw`async [^\s@]*`,
55
+ ].join('|');
56
+ const FRAME_PLACE = [
57
+ String.raw`[a-z][a-z0-9+.-]*:\S+`,
58
+ String.raw`\[native code\]`,
59
+ String.raw`\[wasm code\]`,
60
+ String.raw`(?:[^\s@:[\]]+:)?wasm-function\[\d+\]`,
61
+ ].join('|');
62
+ const AT_FRAME = new RegExp(
63
+ String.raw`^(?:(?:${FRAME_NAME})@(?:${FRAME_PLACE})|\[native code\])$`,
64
+ 'i',
65
+ );
66
+ const STACK_SCAN = 10 * MAX_STACK;
67
+
68
+ const stackFrames = (stack, message) => {
69
+ const lines = clip(stack, STACK_SCAN).split('\n');
70
+
71
+ // обрезка по STACK_SCAN рвёт последнюю строку: неполный кадр не
72
+ // распознала бы ни одна ветка, и весь стек свернулся бы в строку
73
+ if (stack.length > STACK_SCAN && lines.length > 1) {
74
+ lines.pop();
75
+ }
76
+
77
+ // SpiderMonkey/JavaScriptCore: стек — одни кадры, без заголовка; Firefox
78
+ // заканчивает его переводом строки
79
+ let end = lines.length;
80
+
81
+ while (end > 0 && lines[end - 1] === '') {
82
+ end -= 1;
83
+ }
84
+
85
+ const body = lines.slice(0, end);
86
+
87
+ if (body.length > 0 && body.every(line => AT_FRAME.test(line))) {
88
+ return body.join('\n');
89
+ }
90
+
91
+ const rest = lines.slice(message.split('\n').length);
92
+
93
+ if (rest.length > 0 && rest.every(line => V8_FRAME.test(line))) {
94
+ return rest.join('\n');
95
+ }
96
+
97
+ let start = lines.length;
98
+
99
+ while (start > 0 && V8_FRAME.test(lines[start - 1])) {
100
+ start -= 1;
101
+ }
102
+
103
+ return start < lines.length
104
+ ? lines.slice(start).join('\n')
105
+ : lines.join(' | ');
106
+ };
107
+
108
+ // сбой чат-команды → { kind, message, stack }. Не бросает: недоступный
109
+ // message/toString — текст «[unprintable error]», недоступный стек — null.
110
+ // Имя команды приходит из ввода игрока, текст — из кода игры, который вправе
111
+ // вставить в него аргументы игрока: оба приводятся к одной строке и
112
+ // обрезаются. V8 повторяет текст ошибки первой строкой стека — она
113
+ // снимается, остаются кадры
114
+ export function commandFailureReport(command, error) {
115
+ let text;
116
+ let stack = null;
117
+
118
+ try {
119
+ // message читается один раз: геттер мог бы вернуть разное
120
+ const message = error && error.message ? String(error.message) : '';
121
+
122
+ text = message || String(error);
123
+
124
+ if (error && typeof error.stack === 'string') {
125
+ stack = stackFrames(error.stack, message);
126
+ stack = cleanStack(stack) || null;
127
+ }
128
+ } catch {
129
+ text ??= '[unprintable error]';
130
+ stack = null;
131
+ }
132
+
133
+ const commandText = oneLine(command, 32);
134
+
135
+ return {
136
+ kind: 'error',
137
+ message: `chat command ${commandText} failed: ${oneLine(text, 400)}`,
138
+ stack,
139
+ };
140
+ }
@@ -152,6 +152,12 @@ async function onInit(room, handoff = null, checkpointBytes = null, seqFloor) {
152
152
  // ли отдать роль
153
153
  host.setHealthSink(health => self.postMessage({ type: 'health', health }));
154
154
 
155
+ // сбой кода игры, пойманный хостом (чат-команда): в журнал клиентских
156
+ // ошибок тем же сообщением, что и необработанный reject
157
+ host.setDiagnosticSink(report =>
158
+ self.postMessage({ type: 'diagnostic', ...report }),
159
+ );
160
+
155
161
  // в лобби личность игрока — claim identity-токена, проверенного по JWKS
156
162
  // мастера (Этап B3); свободного ввода имени в форме игры нет
157
163
  portMachine = new PortMachine({
@@ -238,7 +244,7 @@ function onDebug({ requestId, action }) {
238
244
  self.postMessage({ type: 'debug_result', requestId, result, error });
239
245
  }
240
246
 
241
- // журнал клиентских ошибок (plan/client-reports): необработанный reject в
247
+ // журнал клиентских ошибок: необработанный reject в
242
248
  // Worker не всплывает в worker.onerror главного потока — пересылаем сами.
243
249
  // Отдельный тип сообщения: 'error' занят сбоем init и запускает откат эстафеты
244
250
  self.addEventListener('unhandledrejection', event => {
@@ -325,7 +331,7 @@ self.onmessage = async event => {
325
331
  host?.setRoom({ roomId: msg.hostId, roomSecret: msg.hostSecret });
326
332
  break;
327
333
 
328
- // отладочный контур (этап 6 плана plan/done/ai-debug): единственный вход в
334
+ // отладочный контур: единственный вход в
329
335
  // авторитетную половину из главного потока — запрос/ответ по requestId
330
336
  case 'debug':
331
337
  onDebug(msg);
@@ -311,8 +311,8 @@ export default class SocketManager {
311
311
  }
312
312
 
313
313
  /**
314
- * Отправка отладочного лога хоста в консоль клиента (порт CONSOLE,
315
- * этап 6 плана plan/done/ai-debug). Worker изолирован от DevTools вкладки —
314
+ * Отправка отладочного лога хоста в консоль клиента (порт CONSOLE).
315
+ * Worker изолирован от DevTools вкладки —
316
316
  * без этого канала его события в браузере не видны вовсе.
317
317
  * @param {string} socketId
318
318
  * @param {*} data
@@ -4,11 +4,19 @@
4
4
  // разных играх может делать разное или отсутствовать вовсе; незнакомая
5
5
  // команда — COMMANDS_NOT_FOUND.
6
6
  //
7
- // Всё, что раньше разбирал движковый switch, доступно играм через контекст:
8
- // roundManager (смена ника, новый раунд, текущая карта), timerManager
9
- // (остаток времени карты), playerDataSync (ранг), chat, isDevMode.
7
+ // Контекст команд: roundManager (новый раунд, текущая карта, смена карты),
8
+ // timerManager (остаток времени карты), playerDataSync (ранг), chat,
9
+ // isDevMode и остальные поля deps.
10
+ //
11
+ // Обработчик — код игры и вызывается под защитой: его сбой (исключение или
12
+ // отклонённый промис) уходит в onError, матч продолжается.
10
13
  class CommandProcessor {
11
- constructor(deps) {
14
+ /**
15
+ * @param {Object} deps - контекст игровых команд.
16
+ * @param {Object} [options]
17
+ * @param {Function} [options.onError] - (command, error) сбой обработчика.
18
+ */
19
+ constructor(deps, { onError = null } = {}) {
12
20
  this._chat = deps.chat;
13
21
 
14
22
  // контекст игровых команд (participants, chat, scripted, roundManager,
@@ -17,6 +25,7 @@ class CommandProcessor {
17
25
  this._ctx = deps;
18
26
 
19
27
  this._commands = new Map();
28
+ this._onError = onError;
20
29
  }
21
30
 
22
31
  /**
@@ -36,10 +45,31 @@ class CommandProcessor {
36
45
  const cmd = arr.shift();
37
46
  const handler = this._commands.get(cmd);
38
47
 
39
- if (handler) {
40
- handler(this._ctx, gameId, arr);
41
- } else {
48
+ if (!handler) {
42
49
  this._chat.pushSystemByUser(gameId, 'COMMANDS_NOT_FOUND');
50
+
51
+ return;
52
+ }
53
+
54
+ // в Worker'е сбой стал бы необработанным reject'ом, в dedicated-процессе —
55
+ // uncaughtException, то есть падением матча всех игроков
56
+ try {
57
+ const result = handler(this._ctx, gameId, arr);
58
+
59
+ if (result && typeof result.then === 'function') {
60
+ result.then(null, error => this._fail(cmd, error));
61
+ }
62
+ } catch (error) {
63
+ this._fail(cmd, error);
64
+ }
65
+ }
66
+
67
+ // сбой приёмника не должен породить новый сбой
68
+ _fail(command, error) {
69
+ try {
70
+ this._onError?.(command, error);
71
+ } catch {
72
+ // приёмник молчит о своих сбоях
43
73
  }
44
74
  }
45
75
  }
@@ -54,7 +54,7 @@ export function applyRoomOverrides(
54
54
  game.timers.mapTime = clampTime(room.mapTime);
55
55
  }
56
56
 
57
- // dev-режим вкладки хоста (этап 6 плана plan/done/ai-debug): включает рекордер
57
+ // dev-режим вкладки хоста: включает рекордер
58
58
  // и хостовый CONSOLE-лог. Прод-бандл его не выставляет — поведение то же
59
59
  if (typeof room.isDevMode === 'boolean') {
60
60
  game.isDevMode = room.isDevMode;
@@ -43,7 +43,7 @@ export const ENGINE_CAPABILITIES = createRegistry('engine-capabilities', [
43
43
  // `SimCtx::map_body_state`) и доступ игры к телам карты по индексу
44
44
  // (хэндл, индекс по хэндлу, отключение тела)
45
45
  { value: 'map.bodyState', since: '0.34.0' },
46
- // сервис пула `diagnostics` (журнал клиентских ошибок, plan/client-reports);
46
+ // сервис пула `diagnostics` (журнал клиентских ошибок);
47
47
  // опционален — игра не пишет его в `requires`
48
48
  { value: 'diagnostics', since: '0.35.0' },
49
49
  // контрольная точка хоста посреди раунда (host-migration этап 5):
@@ -4,8 +4,7 @@
4
4
  //
5
5
  // Контейнер до сжатия: [u32 metaLen LE][meta JSON utf-8][байты дампа ядра];
6
6
  // весь буфер — gzip. Дамп ядра сам по себе JSON (serialize_state), и без
7
- // сжатия точка tanks весит ~420 КБ, с ним ~21 КБ (plan/host-migration,
8
- // spike-results).
7
+ // сжатия точка tanks весит ~420 КБ, с ним ~21 КБ.
9
8
 
10
9
  import hostDefaults from '../config/hostDefaults.js';
11
10
 
@@ -2,7 +2,7 @@ import { SNAPSHOT_FORMAT_VERSION } from '../config/opcodes.js';
2
2
  import hostDefaults from '../config/hostDefaults.js';
3
3
  import wsports from '../config/wsports.js';
4
4
 
5
- // Сборка JSON-конфига Rust-ядра (packages/engine/core + core/ в репозитории
5
+ // Сборка JSON-конфига Rust-ядра (крейт vimp-engine-core + core/ в репозитории
6
6
  // игры, например vimp-tanks): движковая половина
7
7
  // (timeStep/mapScale/mapSetId/snapshot/seed) + игровая
8
8
  // (models/weapons/playerKeys/panel/friendlyFire) — форма {engine, game} из
@@ -87,7 +87,7 @@ export async function createHostRuntime(room, options = {}) {
87
87
  // преемник собирает из них тот же конфиг ядра
88
88
  roomSettings: pickRoomSettings(room),
89
89
  mapsVersion: room?.mapsVersion ?? null,
90
- // рекордер (этап 6 плана plan/done/ai-debug) кладёт seed в сценарий — без него
90
+ // рекордер кладёт seed в сценарий — без него
91
91
  // записанный матч невоспроизводим
92
92
  seed,
93
93
  ...hostOptions,
package/src/lib/gameId.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // Формат id игры — изоморфно: его проверяет мастер (реестр, раздача, каталог)
2
2
  // и клиентский роутер ссылок (client/lib/roomLink.js) до похода в сеть.
3
- // Значение обязано совпадать с packages/auth/src/config/auth.js:games
3
+ // Значение обязано совпадать с games из конфига auth-сервиса
4
4
  // (см. master/gameRefs.js — он реэкспортирует отсюда)
5
5
  export const GAME_ID_PATTERN = /^[a-z][a-z0-9-]{1,30}$/;
6
6
 
@@ -7,8 +7,12 @@ import { createGameConfigView } from './gameConfigView.js';
7
7
  // entries.client из манифеста.
8
8
 
9
9
  // каталог всех игр мастера (GameCatalog, см. Этап 6.2) — массив манифестов
10
- export async function fetchGamesManifest(url = '/games/manifest.json') {
11
- const res = await fetch(url);
10
+ // doFetch подставляет клиент: загрузка страницы повторяет сбой сети
11
+ export async function fetchGamesManifest(
12
+ url = '/games/manifest.json',
13
+ doFetch = fetch,
14
+ ) {
15
+ const res = await doFetch(url);
12
16
 
13
17
  if (!res.ok) {
14
18
  throw new Error(`games manifest: HTTP ${res.status}`);
package/src/lib/jwt.js CHANGED
@@ -41,7 +41,7 @@ export function decodeJwtPayload(token) {
41
41
  }
42
42
 
43
43
  // Авторитетная проверка identity-токена (Этап B3): подпись RS256 по JWKS
44
- // central auth-сервиса (packages/auth), issuer и срок годности. Работает через
44
+ // central auth-сервиса, issuer и срок годности. Работает через
45
45
  // Web Crypto API (crypto.subtle) — доступен в браузере, Worker'е хоста и в
46
46
  // Node ≥19 глобально, отдельной JWT-библиотеки не требует. Возвращает
47
47
  // проверенный payload ({ sub, nick, iss, exp, ... }) или бросает исключение.
@@ -1,63 +1,31 @@
1
- // Ссылка на проект для футеров лобби и формы входа: движок показывает рядом
2
- // npm-версию, и клик по соседней ячейке ведёт на страницу проекта этого пакета.
1
+ // Адрес проекта пакета — его `repository`: ссылка ведёт на саму игру, в том
2
+ // числе закрытую. `homepage` не читается. Фолбэка нет: пакет без repository
3
+ // адреса не получает — страница пакета в npm маскировала бы недостающие
4
+ // метаданные. О пропущенном поле предупреждает правило контракта A7.
3
5
  //
4
- // Источник — `homepage` его package.json (иначе `repository`). Фолбэка нет:
5
- // пакет, не объявивший ни того, ни другого, ссылки не получает, и ячейка
6
- // футера остаётся пустой. Это сознательно — прежний фолбэк на страницу пакета
7
- // в npm давал разнобой («GitHub» у одной игры, «npm» у другой) и молча
8
- // маскировал недостающие метаданные вместо того, чтобы показать их отсутствие.
9
- // О пропущенном поле предупреждает правило контракта A7.
10
- //
11
- // Хостинг не ограничен github.com: подпись выводится из хоста, поэтому чужой
12
- // плагин на GitLab получает честную ссылку, а не пустую ячейку.
13
- //
14
- // Модуль изоморфный: мастер зовёт resolveProjectUrl для пакета игры
15
- // (GameCatalog), клиент — для собственного package.json движка
16
- // (client/lib/engineVersion.js).
6
+ // Хостинг не ограничен github.com. Модуль изоморфный: мастер зовёт
7
+ // resolveProjectUrl для метаданных пакета игры (адрес уходит в реестр игр),
8
+ // devtools — для package.json игры (правило A7).
17
9
 
18
10
  /**
19
- * Адрес проекта из package.json, приведённый к https: homepage, иначе
20
- * repository.
11
+ * Адрес проекта из package.json, приведённый к http(s): поле repository.
21
12
  * @param {Object} pkg - разобранный package.json
22
- * @returns {string|null} https-URL либо null, если поля нет или это не адрес
13
+ * @returns {string|null} http(s)-URL либо null, если поля нет или это не адрес
23
14
  */
24
15
  export function resolveProjectUrl(pkg) {
25
16
  if (!pkg || typeof pkg !== 'object') {
26
17
  return null;
27
18
  }
28
19
 
29
- const { homepage, repository } = pkg;
20
+ const { repository } = pkg;
30
21
 
31
22
  // repository бывает и строкой ("github:user/repo"), и объектом {type, url}
32
- const declared =
33
- typeof repository === 'string' ? repository : repository?.url;
34
-
35
- // homepage важнее repository: поле repository обязано называть
36
- // репозиторий публикации (его сверяет npm Trusted Publishing), и у пакета
37
- // из закрытого репозитория такая ссылка отдаёт игроку 404
38
- return normalize(homepage) ?? normalizeRepository(declared);
39
- }
40
-
41
- /**
42
- * Ссылка футера по готовому адресу проекта.
43
- * @param {string|null} [url] - результат resolveProjectUrl
44
- * @returns {{url: string, label: string}|null} null, если адреса нет
45
- */
46
- export function projectLink(url) {
47
- // адрес перепроверяется, хотя свои вызовы приходят уже из
48
- // resolveProjectUrl: функция — публичный экспорт ("./lib/*"), а результат
49
- // уходит прямо в href якоря (client/lib/footerLink.js)
50
- const value = typeof url === 'string' ? url.trim() : '';
51
-
52
- if (!/^https?:\/\//.test(value)) {
53
- return null;
54
- }
55
-
56
- return { url: value, label: labelFor(value) };
23
+ return normalizeRepository(
24
+ typeof repository === 'string' ? repository : repository?.url,
25
+ );
57
26
  }
58
27
 
59
- // шорткат "user/repo" — семантика поля repository (так его читает npm), в
60
- // homepage такая строка означала бы относительный путь, а не адрес на GitHub
28
+ // шорткат "user/repo" — семантика поля repository (так его читает npm)
61
29
  function normalizeRepository(raw) {
62
30
  const value = typeof raw === 'string' ? raw.trim() : '';
63
31
 
@@ -94,6 +62,9 @@ function normalize(raw) {
94
62
 
95
63
  url = url.replace(/^git\+/, '');
96
64
 
65
+ // порт SSH (ssh://host:2222/…) к адресу https не относится
66
+ url = url.replace(/^(ssh:\/\/(?:[^/@]*@)?[^/:@]+):\d+(?=\/|$)/, '$1');
67
+
97
68
  // scp-форма git@host:user/repo (в том числе с лишним ssh://, как её
98
69
  // нередко пишет npm) — двоеточие здесь разделитель, не порт
99
70
  const scp = /^(?:ssh:\/\/)?git@([^:/]+):(.+)$/.exec(url);
@@ -117,22 +88,8 @@ function normalize(raw) {
117
88
  // в href он уехал бы в DOM и историю браузера
118
89
  .replace(/^(https?:\/\/)[^/@]*@/, '$1')
119
90
  .replace(/\.git(?=$|[#?])/, '')
120
- // npm сам дописывает "#readme" в homepage при генерации package.json
91
+ // якорь #readme в адресе репозитория ссылке не нужен
121
92
  .replace(/#readme$/, '')
122
93
  .replace(/\/+$/, '')
123
94
  );
124
95
  }
125
-
126
- // подпись ячейки: у наших пакетов это всегда GitHub, у чужого хостинга —
127
- // его хост, чтобы подпись не врала об источнике
128
- function labelFor(url) {
129
- let host;
130
-
131
- try {
132
- host = new URL(url).hostname.replace(/^www\./, '');
133
- } catch {
134
- return 'Website';
135
- }
136
-
137
- return host === 'github.com' ? 'GitHub' : host;
138
- }
@@ -13,7 +13,7 @@ const NAME_REGEXP = new RegExp('^[a-zA-Z]([\\w\\s#]{0,13})[\\w]{1}$');
13
13
  export const isValidName = name =>
14
14
  typeof name === 'string' && NAME_REGEXP.test(name);
15
15
 
16
- // ник профиля: копия правила auth-сервиса (packages/auth/src/lib/validators.js)
16
+ // ник профиля: копия правила auth-сервиса (валидаторы auth-сервиса)
17
17
  // — общей рантайм-зависимости у пакетов нет. Строже isValidName: внутри ника
18
18
  // допустим только обычный пробел, не любой \s
19
19
  const NICK_REGEXP = new RegExp('^[a-zA-Z]([\\w #]{0,13})[\\w]{1}$');
@@ -272,8 +272,8 @@ export const clampLimit = (value, fallback, max) => {
272
272
  // на его стороне — минута (lobbyConfig.playerData.minFlushInterval), столько
273
273
  // игр в неё не влезает.
274
274
  //
275
- // Множитель СВЯЗАН с rank.maxPoints auth-сервиса (packages/auth/src/config/
276
- // auth.js): мастер не должен пропускать то, что auth отклонит. Отклонённое
275
+ // Множитель СВЯЗАН с rank.maxPoints из конфига auth-сервиса: мастер
276
+ // не должен пропускать то, что auth отклонит. Отклонённое
277
277
  // тело для хоста неустранимо — PlayerDataSync повторял бы его до конца жизни
278
278
  // комнаты, — поэтому при правке per-game maxGameScore проверяется и потолок
279
279
  // суммы на той стороне.
@@ -10,7 +10,7 @@ import { ensureGameShell } from '../client/views/gameShell.js';
10
10
  // репозитория игры — `npm run dev` открывает вкладку с работающим матчем без
11
11
  // мастера, без входа и без экрана лобби.
12
12
  //
13
- // Отличия от прода (осознанные, см. plan/standalone-sdk/README.md):
13
+ // Отличия от прода (осознанные):
14
14
  // - хост крутится в главном потоке (inline), а не в Worker'е: HostPlugin
15
15
  // непередаваем постмесседжем, а SDK получает живой объект плагина;
16
16
  // - личность гостевая (ник из формы или из playerName), рейтинг и
@@ -1,7 +1,7 @@
1
1
  # gen-api3 — ЗАМОРОЖЕНО
2
2
 
3
3
  Снимок плагина 2026-08-29, поколение `engineApi: 3`.
4
- Не править: правка фикстуры маскирует слом совместимости
5
- (этап 1 плана `plan/plugin-forward-compat`). Новое поколение
4
+ Не править: правка фикстуры маскирует слом совместимости.
5
+ Новое поколение
6
6
  добавляется копированием текущего `../../miniGame`, а не правкой
7
7
  этого каталога.
@@ -1,7 +1,7 @@
1
1
  # gen-api4 — ЗАМОРОЖЕНО
2
2
 
3
3
  Снимок плагина 2026-08-29, поколение `engineApi: 4`.
4
- Не править: правка фикстуры маскирует слом совместимости
5
- (этап 1 плана `plan/plugin-forward-compat`). Новое поколение
4
+ Не править: правка фикстуры маскирует слом совместимости.
5
+ Новое поколение
6
6
  добавляется копированием текущего `../../miniGame`, а не правкой
7
7
  этого каталога.