# СИНГУЛЯР ·22 «СВЯЗЬ» — архитектура

## Назначение
Слой связи над существующими ·18 «СОБЫТИЕ», ·19 «ЧЕЛОВЕК» и ·21 «ОБЩЕНИЕ».
Идея входного прототипа («·20 СВЯЗЬ» чужой среды) принята как ТЗ: CONVERSATION +
CHANNEL + CAPABILITY + FALLBACK + SHARED_ACTIVITY поверх транспортов, которые уже
работают в СИНГУЛЯРЕ, — без переписывания ядра. Модуль собран заново как ·22,
чтобы не ломать занятую нумерацию (·20 ФОНЕТИКА, ·21 ОБЩЕНИЕ).

## Место в системе

```text
ЧЕЛОВЕК ·19 (личность, адаптация)
     │
СОБЫТИЕ ·18 (зал по коду/QR, WebRTC mesh, память)
     │
СВЯЗЬ ·22 (каналы, fallback, журнал)   ←── представления: ·21 ОБЩЕНИЕ
     │
ПАМЯТЬ (журнал и записи — на устройстве человека)
```

## Контракт

```text
зал ≠ аккаунт          — вход по коду, без учётных записей
канал ≠ личность       — способ связи не создаёт профиль
сервер ≠ хозяин речи   — транспорт удобство, не власть
недоступность ≠ исключение — fallback ведёт к базовому каналу
выбор человека ≥ автоматика — override всегда выигрывает
```

## Каналы (реестр CHANNELS — единственное место с человеческими именами)

| Канал | Требования | Путь fallback |
|---|---|---|
| Текст | нет | — (базовый) |
| Файл | нет | → Текст |
| Субтитры | нет (крупный показ входящих) | → Текст |
| Голосовое | микрофон | → Текст |
| Озвучка | синтез речи | → Текст |
| Аудиозвонок | микрофон, сеть | → Голосовое → Текст |
| Видеозвонок | микрофон, камера, сеть | → Аудиозвонок → Голосовое → Текст |
| Экран | доступ к экрану, сеть | → Субтитры → Текст |
| Совместное пение | нет (открывает зал ·18) | → Голосовое → Текст |

«Текст» доступен без устройств, поэтому ни один путь не кончается тупиком.
Свойство проверяется тестом перебором всех 128 комбинаций возможностей × 9 каналов.

## Мосты (реальные, не декоративные)

- **·18 СОБЫТИЕ**: кнопка «ссылка на зал ·18 с этим кодом» и канал «Совместное
  пение» открывают `СИНГУЛЯР_18_СОБЫТИЕ.html?зал=КОД` — тот же контракт, что у
  моста ·19 → ·18. Вход по ссылке `?зал=КОД` поддержан на boot.
- **·19 ЧЕЛОВЕК**: имя берётся из локальной личности (IndexedDB
  `singulyar-human-v1`, ключ `личность`) только на чтение; если мост случайно
  создал пустую базу при чтении, он её удаляет, чтобы не сломать ·19.
  Адаптация (`singulyar.adaptation.v1`) уважается: «меньше движения» и «крупные
  цели» применяются как «любое да выигрывает» вместе с профилем ·22.
- **·21 ОБЩЕНИЕ**: персонализация голоса (конкретный голос, язык, темп, высота,
  громкость) остаётся в ·21; субтитры и озвучка ·22 — каналы связи, а не
  дубликат маршрутизатора представлений.

## Транспорт — честные границы

- Локальный канал: BroadcastChannel `singular-svyaz-v1-КОД` связывает окна одного
  браузера. Голосовые сообщения и файлы передаются как Blob (structured clone).
- Звонок меж устройствами (v1.24): у ·22 свой ретранслятор — trystero/nostr
  (публичные релеи, то же семейство транспорта, что у зала ·18; отдельное
  пространство имён `singular-svyaz-call-v1`, залы не смешиваются). По реле едут
  ТОЛЬКО факты звонка (звонок), рукопожатие (звонок-sdp/звонок-ice) и
  приветствия (имя/публичныйJWK/подпись). Медиа — прямой WebRTC между
  устройствами с публичным STUN. Дедуп провода по ид сообщения (кольцо):
  двойная доставка (BroadcastChannel + реле) не дублирует факты; сообщения без
  ид (старые вкладки) проходят как раньше. Поздний пир получает приветствие
  перегритингом на onPeerJoin. Сигналинг и media transport — разные слои;
  СМЫСЛ ≠ ТРАНСПОРТ: SDP/ICE в журнал не пишутся.
- Запечатанный текст меж устройствами (v1.25): ВТОРАЯ машина транспорта —
  тот же механизм, СВОЁ пространство имён `singular-svyaz-chat-v1` (залы текста
  не смешаны ни с залами звонка, ни с залами ·18). По реле едет ТОЛЬКО
  шифровка (шф AES-GCM, печать несёт эпоху; вскрытие картой эпох готовой
  крипты v2); ОТКРЫТЫЙ текст по публичным релеям не ездит вообще — правило
  `S22.текстНаРеле(запечатано)`: без печати сообщение остаётся на локальном
  проводе. Голос/файл меж устройствами — следующий такт (большие payload’ы —
  отдельная работа). Приём идёт через общий `принять`: дедуп по ми (общее
  кольцо обеих машин), durable журнал, субтитры/озвучка/мост ·21 — все
  существующие пути не тронуты. Приветствие едет в ОБЕ машины (ми тот же —
  дедуп бережёт от шторма), перегритинг на onPeerJoin у каждой.
- Честные границы меж устройства: TURN не входит (за симметричным NAT напрямую
  может не пробиться — TURN-надстройка зала ·18); межустройственный текст —
  ТОЛЬКО запечатанный (открытый текст по публичным релеям не ездит сознанно);
  голос/файл меж устройствами — следующий такт; история на релеях не хранится
  (пространство живёт пока открыт зал, журнал — только на устройствах);
  звонок адресуется залу, не устройству (семантика v1.23).

## Память

IndexedDB `singular-svyaz` (stores: `journal`, `blobs`). Экспорт выгружает текст
и метаданные в JSON; звук и файлы остаются на устройстве. Удаление всего —
отдельное действие человека.

## Не делать (наследие прототипа, подтверждено)

- обязательную регистрацию, телефон, email;
- CAPTCHA как условие коммуникации, обязательную MFA, биометрию;
- рекламу и телеметрию;
- автоматическое определение «типа человека» и режимы «для слепого/глухого»;
- принудительный TTS/ASR;
- выдавать SpeechRecognition браузера за доказанно локальный;
- обещать P2P там, где NAT требует TURN (за жёстким симметричным NAT голос
  может не пробиться — TURN в дорожной карте).

## Масштабирование

Mesh подходит малым группам (~4–5 участников). При росте комнаты media-plane
заменяется на SFU без изменения пользовательской модели EVENT/CONVERSATION/CHANNEL.

## Доступность

WCAG 2.2: клавиатурная доступность, видимый фокус (`outline: 3px`),
крупные цели (кнопки ≥ 44 px, с профилем «крупные кнопки» — ≥ 56 px),
`aria-live` для журнала и субтитров, честные статусы вместо ярлыков.

## Тестовый статус

- Логика: `ИНСТРУМЕНТЫ/test_s22_logic.js` — 135/135 (ядро 1.4.0: дедуп провода,
  машина состояний, каналы/fallback, v1.22-верификация, v1.23-звонок,
  v1.25-текстНаРеле + своё пространство текста);
  ядро исполняется из отгруженного файла (маркеры S22:CORE-BEGIN/END).
- Браузер: `ИНСТРУМЕНТЫ/test_s22_browser.py` — 67/67 (Playwright, fake-микрофон):
  канал между окнами (текст/голос/файл), субтитры, профиль, мосты ·19/·18,
  `?зал=КОД`, полный цикл звонка в одном браузере, 0 ошибок консоли.
- Межустройство (v1.24): `ИНСТРУМЕНТЫ/test_s22_device.py` — 28/28: два
  ИЗОЛИРОВАННЫХ контекста (BroadcastChannel между ними не ходит — проба в тесте)
  через РЕАЛЬНЫЕ публичные релеи Nostr: приветствия с верификацией ·19, полный
  цикл ring→accept→разговор→end, WebRTC-медиа между контекстами, отклон,
  факты call.signal в журналах обеих сторон, дедуп, 0 ошибок консоли
  (сбои отдельных wss-релеев — штатная деградация redundancy=2, фильтруются
  явно и задокументированы).
- Межустройство ТЕКСТ (v1.25): `ИНСТРУМЕНТЫ/test_s22_chat_device.py` — 21/21:
  два изолированных контекста через РЕАЛЬНЫЕ релеи в пространстве
  `singular-svyaz-chat-v1`: запечатанный текст доехал и вскрылся картой эпох,
  третий контекст БЕЗ фразы («Европа») получил шифровку и видит честное
  «запечатано», открытый текст между устройствами НЕ ездит, дедуп без копий,
  0 ошибок консоли.
- UNKNOWN: реальный звонок через TURN (симметричный NAT) — нужен внешний хост;
  звонок между разными физическими устройствами людей — транспорт проверен
  изолированными контекстами, полевые звонки — за человеком.

## Мост ·21⇄·22 (редакция 1.1, v1)

Живой канал между открытыми вкладками ·22 и ·21 одного браузера
(BroadcastChannel `singular-most-21-22-v1`). Представления строит ·21 у
получателя — ·22 не дублирует его логику; в зал уходит только смысл
(смысл ≠ представление). Пока вкладка соседа закрыта, мост молчит — это
не поломка, зал и журнал работают.

Контракт конверта: `{мост:'21-22', v:1, тип, t, …}`. Сообщения:

- `hello {side, эхо, н}` — зов вкладки. Ответ только на пустое `эхо`,
  в ответе `эхо = н` зова: на эхо не отвечают, петля невозможна без таймеров.
- `bye {side}` — вкладка закрывается (pagehide), сосед честно помечает молчание.
- `incoming {зал, от, текст}` (·22 → ·21) — входящий текст зала предложен
  на представление; ·21 строит `S21.route(получатель, текст)`, показывает
  карточку (текст/речь/Брайль/вибро + кнопка «Произнести голосом получателя»)
  и отвечает `presented`.
- `presented {зал, от, представления[]}` (·21 → ·22) — подтверждение
  построенных представлений; ·22 пишет в журнал моста, ядро зала не трогает.
- `tohall {от, текст}` (·21 → ·22) — «В зал ·22»: смысл попадает в журнал
  связи под подписью «·21 · имя», субтитры/озвучка/вибро — по профилю ·22.
  ·22 НЕ переизлучает `tohall` в зал и мост — петли исключены конструктивно.

Правила честности: ни одна сторона не переизлучает чужие сообщения; мост
ничего не пишет в память (журнал моста живёт, пока открыты вкладки); имя
получателя и голос берутся из данных ·21, без угадывания.

Тесты редакции 1.1: `ИНСТРУМЕНТЫ/test_bridge_logic.js` — 33/33 (контракт
симметрии, отсутствие петель, ядра S21/S22 из отгруженных файлов);
`ИНСТРУМЕНТЫ/test_bridge_browser.py` — 15/15 (Playwright: два окна ·22
одного зала + ·21, рукопожатие, оба направления, субтитры, 0 ошибок консоли).
